mirror of
https://github.com/yairm210/Unciv.git
synced 2025-07-11 16:29:48 +07:00
Modmanager sort and filter (#5186)
* 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 sorts and filters - WIP 1 * Mod Manager sorts and filters - WIP 2 * Mod Manager sorts and filters - WIP 2a * Mod Manager sorts and filters - WIP 3 * Mod Manager sorts and filters - atlas * Mod Manager sorts and filters - tip
This commit is contained in:
BIN
android/Images/OtherIcons/Search.png
Normal file
BIN
android/Images/OtherIcons/Search.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.8 KiB |
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Before Width: | Height: | Size: 1002 KiB After Width: | Height: | Size: 1016 KiB |
@ -1210,6 +1210,17 @@ No description provided =
|
|||||||
Author: [author] =
|
Author: [author] =
|
||||||
Size: [size] kB =
|
Size: [size] kB =
|
||||||
The mod you selected is incompatible with the defined ruleset! =
|
The mod you selected is incompatible with the defined ruleset! =
|
||||||
|
Sort and Filter =
|
||||||
|
Filter: =
|
||||||
|
Enter search text =
|
||||||
|
Sort Current: =
|
||||||
|
Sort Downloadable: =
|
||||||
|
Name ↑ =
|
||||||
|
Name ↓ =
|
||||||
|
Date ↑ =
|
||||||
|
Date ↓ =
|
||||||
|
Stars ↓ =
|
||||||
|
Status ↓ =
|
||||||
|
|
||||||
# Uniques that are relevant to more than one type of game object
|
# Uniques that are relevant to more than one type of game object
|
||||||
|
|
||||||
|
@ -264,6 +264,13 @@ class TranslatedSelectBox(values : Collection<String>, default:String, skin: Ski
|
|||||||
class TranslatedString(val value: String) {
|
class TranslatedString(val value: String) {
|
||||||
val translation = value.tr()
|
val translation = value.tr()
|
||||||
override fun toString() = translation
|
override fun toString() = translation
|
||||||
|
// Equality contract needs to be implemented else TranslatedSelectBox.setSelected won't work properly
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (javaClass != other?.javaClass) return false
|
||||||
|
return value == (other as TranslatedString).value
|
||||||
|
}
|
||||||
|
override fun hashCode() = value.hashCode()
|
||||||
}
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
234
core/src/com/unciv/ui/pickerscreens/ModManagementOptions.kt
Normal file
234
core/src/com/unciv/ui/pickerscreens/ModManagementOptions.kt
Normal file
@ -0,0 +1,234 @@
|
|||||||
|
package com.unciv.ui.pickerscreens
|
||||||
|
|
||||||
|
import com.badlogic.gdx.graphics.Color
|
||||||
|
import com.badlogic.gdx.scenes.scene2d.Touchable
|
||||||
|
import com.badlogic.gdx.scenes.scene2d.ui.*
|
||||||
|
import com.badlogic.gdx.utils.Align
|
||||||
|
import com.unciv.models.ruleset.Ruleset
|
||||||
|
import com.unciv.models.translations.tr
|
||||||
|
import com.unciv.ui.newgamescreen.TranslatedSelectBox
|
||||||
|
import com.unciv.ui.utils.*
|
||||||
|
import com.unciv.ui.utils.UncivTooltip.Companion.addTooltip
|
||||||
|
import com.unciv.ui.worldscreen.mainmenu.Github
|
||||||
|
import kotlin.math.sign
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper class for Mod Manager - filtering and sorting.
|
||||||
|
*
|
||||||
|
* This isn't a UI Widget, but offers one: [expander] can be used to offer filtering and sorting options.
|
||||||
|
* It holds the variables [sortInstalled] and [sortOnline] for the [modManagementScreen] and knows
|
||||||
|
* how to sort collections of [ModUIData] by providing comparators.
|
||||||
|
*/
|
||||||
|
class ModManagementOptions(private val modManagementScreen: ModManagementScreen) {
|
||||||
|
companion object {
|
||||||
|
val sortByName = Comparator { mod1, mod2: ModUIData -> mod1.name.compareTo(mod2.name, true) }
|
||||||
|
val sortByNameDesc = Comparator { mod1, mod2: ModUIData -> mod2.name.compareTo(mod1.name, true) }
|
||||||
|
// lastUpdated is compared as string, but that should be OK as it's ISO format
|
||||||
|
val sortByDate = Comparator { mod1, mod2: ModUIData -> mod1.lastUpdated().compareTo(mod2.lastUpdated()) }
|
||||||
|
val sortByDateDesc = Comparator { mod1, mod2: ModUIData -> mod2.lastUpdated().compareTo(mod1.lastUpdated()) }
|
||||||
|
// comparators for stars or status
|
||||||
|
val sortByStars = Comparator { mod1, mod2: ModUIData ->
|
||||||
|
10 * (mod2.stargazers() - mod1.stargazers()) + mod1.name.compareTo(mod2.name, true).sign
|
||||||
|
}
|
||||||
|
val sortByStatus = Comparator { mod1, mod2: ModUIData ->
|
||||||
|
10 * (mod2.state.sortWeight() - mod1.state.sortWeight()) + mod1.name.compareTo(mod2.name, true).sign
|
||||||
|
}
|
||||||
|
|
||||||
|
const val installedHeaderText = "Current mods"
|
||||||
|
const val onlineHeaderText = "Downloadable mods"
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class SortType(
|
||||||
|
val label: String,
|
||||||
|
val symbols: String,
|
||||||
|
val comparator: Comparator<in ModUIData>
|
||||||
|
) {
|
||||||
|
Name("Name ↑", "↑", sortByName),
|
||||||
|
NameDesc("Name ↓", "↓", sortByNameDesc),
|
||||||
|
Date("Date ↑", "⌚↑", sortByDate),
|
||||||
|
DateDesc("Date ↓", "⌚↓", sortByDateDesc),
|
||||||
|
Stars("Stars ↓", "✯↓", sortByStars),
|
||||||
|
Status("Status ↓", "◉↓", sortByStatus);
|
||||||
|
|
||||||
|
fun next() = values()[(ordinal + 1) % values().size]
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun fromSelectBox(selectBox: TranslatedSelectBox): SortType {
|
||||||
|
val selected = selectBox.selected.value
|
||||||
|
return values().firstOrNull { it.label == selected } ?: Name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val textField = TextField("", CameraStageBaseScreen.skin)
|
||||||
|
fun getFilterText(): String = textField.text
|
||||||
|
val filterAction: ()->Unit
|
||||||
|
|
||||||
|
var sortInstalled = SortType.Name
|
||||||
|
var sortOnline = SortType.Stars
|
||||||
|
|
||||||
|
private val sortInstalledSelect: TranslatedSelectBox
|
||||||
|
private val sortOnlineSelect: TranslatedSelectBox
|
||||||
|
|
||||||
|
var expanderChangeEvent: (()->Unit)? = null
|
||||||
|
val expander: ExpanderTab
|
||||||
|
|
||||||
|
init {
|
||||||
|
textField.messageText = "Enter search text"
|
||||||
|
|
||||||
|
val searchIcon = ImageGetter.getImage("OtherIcons/Search")
|
||||||
|
.surroundWithCircle(50f, color = Color.CLEAR)
|
||||||
|
|
||||||
|
sortInstalledSelect = TranslatedSelectBox(
|
||||||
|
SortType.values().filter { sort -> sort != SortType.Stars }.map { sort -> sort.label },
|
||||||
|
sortInstalled.label,
|
||||||
|
CameraStageBaseScreen.skin
|
||||||
|
)
|
||||||
|
sortInstalledSelect.onChange {
|
||||||
|
sortInstalled = SortType.fromSelectBox(sortInstalledSelect)
|
||||||
|
modManagementScreen.refreshInstalledModTable()
|
||||||
|
}
|
||||||
|
|
||||||
|
sortOnlineSelect = TranslatedSelectBox(
|
||||||
|
SortType.values().map { sort -> sort.label },
|
||||||
|
sortOnline.label,
|
||||||
|
CameraStageBaseScreen.skin
|
||||||
|
)
|
||||||
|
sortOnlineSelect.onChange {
|
||||||
|
sortOnline = SortType.fromSelectBox(sortOnlineSelect)
|
||||||
|
modManagementScreen.refreshOnlineModTable()
|
||||||
|
}
|
||||||
|
|
||||||
|
expander = ExpanderTab(
|
||||||
|
"Sort and Filter",
|
||||||
|
fontSize = 18,
|
||||||
|
startsOutOpened = false,
|
||||||
|
defaultPad = 2.5f,
|
||||||
|
headerPad = 5f,
|
||||||
|
expanderWidth = 360f,
|
||||||
|
onChange = { expanderChangeEvent?.invoke() }
|
||||||
|
) {
|
||||||
|
it.background = ImageGetter.getBackground(Color(0x203050ff))
|
||||||
|
it.pad(7.5f)
|
||||||
|
it.add(Table().apply {
|
||||||
|
add("Filter:".toLabel()).left()
|
||||||
|
add(textField).pad(0f, 5f, 0f, 5f).growX()
|
||||||
|
add(searchIcon).right()
|
||||||
|
}).colspan(2).growX().padBottom(7.5f).row()
|
||||||
|
it.add("Sort Current:".toLabel()).left()
|
||||||
|
it.add(sortInstalledSelect).right().padBottom(7.5f).row()
|
||||||
|
it.add("Sort Downloadable:".toLabel()).left()
|
||||||
|
it.add(sortOnlineSelect).right().row()
|
||||||
|
}
|
||||||
|
|
||||||
|
searchIcon.touchable = Touchable.enabled
|
||||||
|
filterAction = {
|
||||||
|
if (expander.isOpen) {
|
||||||
|
modManagementScreen.refreshInstalledModTable()
|
||||||
|
modManagementScreen.refreshOnlineModTable()
|
||||||
|
} else {
|
||||||
|
modManagementScreen.stage.keyboardFocus = textField
|
||||||
|
}
|
||||||
|
expander.toggle()
|
||||||
|
}
|
||||||
|
searchIcon.onClick(filterAction)
|
||||||
|
searchIcon.addTooltip(KeyCharAndCode.RETURN, 18f)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getInstalledHeader() = installedHeaderText.tr() + " " + sortInstalled.symbols
|
||||||
|
fun getOnlineHeader() = onlineHeaderText.tr() + " " + sortOnline.symbols
|
||||||
|
|
||||||
|
fun installedHeaderClicked() {
|
||||||
|
do {
|
||||||
|
sortInstalled = sortInstalled.next()
|
||||||
|
} while (sortInstalled == SortType.Stars)
|
||||||
|
sortInstalledSelect.selected = TranslatedSelectBox.TranslatedString(sortInstalled.label)
|
||||||
|
modManagementScreen.refreshInstalledModTable()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onlineHeaderClicked() {
|
||||||
|
sortOnline = sortOnline.next()
|
||||||
|
sortOnlineSelect.selected = TranslatedSelectBox.TranslatedString(sortOnline.label)
|
||||||
|
modManagementScreen.refreshOnlineModTable()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Helper class holds combined mod info for ModManagementScreen, used for both installed and online lists */
|
||||||
|
class ModUIData(
|
||||||
|
val name: String,
|
||||||
|
val description: String,
|
||||||
|
val ruleset: Ruleset?,
|
||||||
|
val repo: Github.Repo?,
|
||||||
|
var y: Float,
|
||||||
|
var height: Float,
|
||||||
|
var button: Button
|
||||||
|
) {
|
||||||
|
var state = ModStateImages() // visible only on the 'installed' side - todo?
|
||||||
|
|
||||||
|
constructor(ruleset: Ruleset): this (
|
||||||
|
ruleset.name,
|
||||||
|
ruleset.getSummary().let {
|
||||||
|
"Installed".tr() + (if (it.isEmpty()) "" else ": $it")
|
||||||
|
},
|
||||||
|
ruleset, null, 0f, 0f, ruleset.name.toTextButton()
|
||||||
|
)
|
||||||
|
|
||||||
|
constructor(repo: Github.Repo, isUpdated: Boolean): this (
|
||||||
|
repo.name,
|
||||||
|
(repo.description ?: "-{No description provided}-".tr()) +
|
||||||
|
"\n" + "[${repo.stargazers_count}]✯".tr(),
|
||||||
|
null, repo, 0f, 0f,
|
||||||
|
(repo.name + (if (isUpdated) " - {Updated}" else "" )).toTextButton()
|
||||||
|
) {
|
||||||
|
state.isUpdated = isUpdated
|
||||||
|
}
|
||||||
|
|
||||||
|
fun lastUpdated() = ruleset?.modOptions?.lastUpdated ?: repo?.updated_at ?: ""
|
||||||
|
fun stargazers() = repo?.stargazers_count ?: 0
|
||||||
|
fun author() = ruleset?.modOptions?.author ?: repo?.owner?.login ?: ""
|
||||||
|
fun matchesFilter(filterText: String): Boolean = when {
|
||||||
|
filterText.isEmpty() -> true
|
||||||
|
name.contains(filterText, true) -> true
|
||||||
|
// description.contains(filterText, true) -> true // too many surprises as description is different in the two columns
|
||||||
|
author().contains(filterText, true) -> true
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Helper class keeps references to decoration images of installed mods to enable dynamic visibility
|
||||||
|
* (actually we do not use isVisible but refill a container selectively which allows the aggregate height to adapt and the set to center vertically)
|
||||||
|
* @param visualImage image indicating _enabled as permanent visual mod_
|
||||||
|
* @param updatedImage image indicating _online mod has been updated_
|
||||||
|
*/
|
||||||
|
class ModStateImages (
|
||||||
|
isVisual: Boolean = false,
|
||||||
|
isUpdated: Boolean = false,
|
||||||
|
val visualImage: Image = ImageGetter.getImage("UnitPromotionIcons/Scouting"),
|
||||||
|
val updatedImage: Image = ImageGetter.getImage("OtherIcons/Mods")
|
||||||
|
) {
|
||||||
|
/** The table containing the indicators (one per mod, narrow, arranges up to three indicators vertically) */
|
||||||
|
val container: Table = Table().apply { defaults().size(20f).align(Align.topLeft) }
|
||||||
|
// mad but it's really initializing with the primary constructor parameter and not calling update()
|
||||||
|
var isVisual: Boolean = isVisual
|
||||||
|
set(value) { if (field!=value) { field = value; update() } }
|
||||||
|
var isUpdated: Boolean = isUpdated
|
||||||
|
set(value) { if (field!=value) { field = value; update() } }
|
||||||
|
private val spacer = Table().apply { width = 20f; height = 0f }
|
||||||
|
|
||||||
|
fun update() {
|
||||||
|
container.run {
|
||||||
|
clear()
|
||||||
|
if (isVisual) add(visualImage).row()
|
||||||
|
if (isUpdated) add(updatedImage).row()
|
||||||
|
if (!isVisual && !isUpdated) add(spacer)
|
||||||
|
pack()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun sortWeight() = when {
|
||||||
|
isUpdated && isVisual -> 3
|
||||||
|
isUpdated -> 2
|
||||||
|
isVisual -> 1
|
||||||
|
else -> 0
|
||||||
|
}
|
||||||
|
}
|
@ -15,25 +15,32 @@ import com.unciv.models.ruleset.Ruleset
|
|||||||
import com.unciv.models.ruleset.RulesetCache
|
import com.unciv.models.ruleset.RulesetCache
|
||||||
import com.unciv.models.translations.tr
|
import com.unciv.models.translations.tr
|
||||||
import com.unciv.ui.utils.*
|
import com.unciv.ui.utils.*
|
||||||
|
import com.unciv.ui.pickerscreens.ModManagementOptions.SortType
|
||||||
import com.unciv.ui.utils.UncivDateFormat.formatDate
|
import com.unciv.ui.utils.UncivDateFormat.formatDate
|
||||||
import com.unciv.ui.utils.UncivDateFormat.parseDate
|
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.collections.HashMap
|
||||||
import kotlin.concurrent.thread
|
import kotlin.concurrent.thread
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The Mod Management Screen - called only from [MainMenuScreen]
|
* The Mod Management Screen - called only from [MainMenuScreen]
|
||||||
|
* @param previousOnlineMods - cached online mod list, if supplied and not empty, it will be displayed as is and no online query will be run. Used for resize.
|
||||||
*/
|
*/
|
||||||
// All picker screens auto-wrap the top table in a ScrollPane.
|
// All picker screens auto-wrap the top table in a ScrollPane.
|
||||||
// Since we want the different parts to scroll separately, we disable the default ScrollPane, which would scroll everything at once.
|
// Since we want the different parts to scroll separately, we disable the default ScrollPane, which would scroll everything at once.
|
||||||
class ModManagementScreen: PickerScreen(disableScroll = true) {
|
class ModManagementScreen(
|
||||||
|
previousInstalledMods: HashMap<String, ModUIData>? = null,
|
||||||
|
previousOnlineMods: HashMap<String, ModUIData>? = null
|
||||||
|
): PickerScreen(disableScroll = true) {
|
||||||
|
|
||||||
private val modTable = Table().apply { defaults().pad(10f) }
|
private val modTable = Table().apply { defaults().pad(10f) }
|
||||||
private val scrollInstalledMods = AutoScrollPane(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 = AutoScrollPane(downloadTable)
|
private val scrollOnlineMods = AutoScrollPane(downloadTable)
|
||||||
private val modActionTable = Table().apply { defaults().pad(10f) }
|
private val modActionTable = Table().apply { defaults().pad(10f) }
|
||||||
|
private val optionsManager = ModManagementOptions(this)
|
||||||
|
|
||||||
val amountPerPage = 30
|
val amountPerPage = 30
|
||||||
|
|
||||||
@ -46,24 +53,18 @@ class ModManagementScreen: PickerScreen(disableScroll = true) {
|
|||||||
private val deprecationCell: Cell<WrappableLabel>
|
private val deprecationCell: Cell<WrappableLabel>
|
||||||
private val modDescriptionLabel: WrappableLabel
|
private val modDescriptionLabel: WrappableLabel
|
||||||
|
|
||||||
|
private var installedHeaderLabel: Label? = null
|
||||||
|
private var onlineHeaderLabel: Label? = null
|
||||||
|
private var installedExpanderTab: ExpanderTab? = null
|
||||||
|
private var onlineExpanderTab: ExpanderTab? = null
|
||||||
|
|
||||||
// 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
|
||||||
|
|
||||||
// Description data from installed mods and online search
|
// Enable re-sorting and syncing entries in 'installed' and 'repo search' ScrollPanes
|
||||||
private val modDescriptionsInstalled: HashMap<String, String> = hashMapOf()
|
private val installedModInfo = previousInstalledMods ?: HashMap(10) // HashMap<String, ModUIData> inferred
|
||||||
private val modDescriptionsOnline: HashMap<String, String> = hashMapOf()
|
private val onlineModInfo = previousOnlineMods ?: HashMap(90) // HashMap<String, ModUIData> inferred
|
||||||
private fun showModDescription(modName: String) {
|
|
||||||
val online = modDescriptionsOnline[modName] ?: ""
|
|
||||||
val installed = modDescriptionsInstalled[modName] ?: ""
|
|
||||||
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
|
|
||||||
private class ScrollToEntry(val y: Float, val height: Float, val button: Button)
|
|
||||||
private val installedScrollIndex = 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
|
||||||
@ -76,37 +77,6 @@ class ModManagementScreen: PickerScreen(disableScroll = true) {
|
|||||||
super.dispose()
|
super.dispose()
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Helper class keeps references to decoration images of installed mods to enable dynamic visibility
|
|
||||||
* (actually we do not use isVisible but refill a container selectively which allows the aggregate height to adapt and the set to center vertically)
|
|
||||||
* @param container the table containing the indicators (one per mod, narrow, arranges up to three indicators vertically)
|
|
||||||
* @param visualImage image indicating _enabled as permanent visual mod_
|
|
||||||
* @param updatedImage image indicating _online mod has been updated_
|
|
||||||
*/
|
|
||||||
private class ModStateImages (
|
|
||||||
val container: Table,
|
|
||||||
isVisual: Boolean = false,
|
|
||||||
isUpdated: Boolean = false,
|
|
||||||
val visualImage: Image = ImageGetter.getImage("UnitPromotionIcons/Scouting"),
|
|
||||||
val updatedImage: Image = ImageGetter.getImage("OtherIcons/Mods")
|
|
||||||
) {
|
|
||||||
// mad but it's really initializing with the primary constructor parameter and not calling update()
|
|
||||||
var isVisual: Boolean = isVisual
|
|
||||||
set(value) { if(field!=value) { field = value; update() } }
|
|
||||||
var isUpdated: Boolean = isUpdated
|
|
||||||
set(value) { if(field!=value) { field = value; update() } }
|
|
||||||
private val spacer = Table().apply { width = 20f; height = 0f }
|
|
||||||
fun update() {
|
|
||||||
container.run {
|
|
||||||
clear()
|
|
||||||
if (isVisual) add(visualImage).row()
|
|
||||||
if (isUpdated) add(updatedImage).row()
|
|
||||||
if (!isVisual && !isUpdated) add(spacer)
|
|
||||||
pack()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
private val modStateImages = HashMap<String,ModStateImages>(30)
|
|
||||||
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
//setDefaultCloseAction(screen) // this would initialize the new MainMenuScreen immediately
|
//setDefaultCloseAction(screen) // this would initialize the new MainMenuScreen immediately
|
||||||
@ -122,7 +92,7 @@ class ModManagementScreen: PickerScreen(disableScroll = true) {
|
|||||||
onBackButtonClicked(closeAction)
|
onBackButtonClicked(closeAction)
|
||||||
|
|
||||||
val labelWidth = max(stage.width / 2f - 60f,60f)
|
val labelWidth = max(stage.width / 2f - 60f,60f)
|
||||||
deprecationLabel = WrappableLabel("Deprecated until update conforms to current requirements", labelWidth, Color.FIREBRICK)
|
deprecationLabel = WrappableLabel(deprecationText, labelWidth, Color.FIREBRICK)
|
||||||
deprecationLabel.wrap = true
|
deprecationLabel.wrap = true
|
||||||
modDescriptionLabel = WrappableLabel("", labelWidth)
|
modDescriptionLabel = WrappableLabel("", labelWidth)
|
||||||
modDescriptionLabel.wrap = true
|
modDescriptionLabel.wrap = true
|
||||||
@ -143,19 +113,28 @@ class ModManagementScreen: PickerScreen(disableScroll = true) {
|
|||||||
if (isNarrowerThan4to3()) initPortrait()
|
if (isNarrowerThan4to3()) initPortrait()
|
||||||
else initLandscape()
|
else initLandscape()
|
||||||
|
|
||||||
reloadOnlineMods()
|
keyPressDispatcher[KeyCharAndCode.RETURN] = optionsManager.filterAction
|
||||||
|
|
||||||
|
if (onlineModInfo.isEmpty())
|
||||||
|
reloadOnlineMods()
|
||||||
|
else
|
||||||
|
refreshOnlineModTable()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initPortrait() {
|
private fun initPortrait() {
|
||||||
topTable.defaults().top().pad(0f)
|
topTable.defaults().top().pad(0f)
|
||||||
|
|
||||||
topTable.add(ExpanderTab("Current mods", expanderWidth = stage.width) {
|
topTable.add(optionsManager.expander).top().growX().row()
|
||||||
it.add(scrollInstalledMods).growX()
|
|
||||||
}).top().growX().row()
|
|
||||||
|
|
||||||
topTable.add(ExpanderTab("Downloadable mods", expanderWidth = stage.width) {
|
installedExpanderTab = ExpanderTab(optionsManager.getInstalledHeader(), expanderWidth = stage.width) {
|
||||||
|
it.add(scrollInstalledMods).growX()
|
||||||
|
}
|
||||||
|
topTable.add(installedExpanderTab).top().growX().row()
|
||||||
|
|
||||||
|
onlineExpanderTab = ExpanderTab(optionsManager.getOnlineHeader(), expanderWidth = stage.width) {
|
||||||
it.add(scrollOnlineMods).growX()
|
it.add(scrollOnlineMods).growX()
|
||||||
}).top().padTop(10f).growX().row()
|
}
|
||||||
|
topTable.add(onlineExpanderTab).top().padTop(10f).growX().row()
|
||||||
|
|
||||||
topTable.add().expandY().row() // helps with top() being ignored
|
topTable.add().expandY().row() // helps with top() being ignored
|
||||||
|
|
||||||
@ -167,9 +146,17 @@ class ModManagementScreen: PickerScreen(disableScroll = true) {
|
|||||||
private fun initLandscape() {
|
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)
|
installedHeaderLabel = optionsManager.getInstalledHeader().toLabel()
|
||||||
|
installedHeaderLabel!!.onClick {
|
||||||
|
optionsManager.installedHeaderClicked()
|
||||||
|
}
|
||||||
|
topTable.add(installedHeaderLabel).pad(5f).minWidth(200f).padLeft(25f)
|
||||||
// 30 = 5 default pad + 20 to compensate for 'permanent visual mod' decoration icon
|
// 30 = 5 default pad + 20 to compensate for 'permanent visual mod' decoration icon
|
||||||
topTable.add("Downloadable mods".toLabel()).pad(5f)
|
onlineHeaderLabel = optionsManager.getOnlineHeader().toLabel()
|
||||||
|
onlineHeaderLabel!!.onClick {
|
||||||
|
optionsManager.onlineHeaderClicked()
|
||||||
|
}
|
||||||
|
topTable.add(onlineHeaderLabel).pad(5f)
|
||||||
topTable.add("".toLabel()).minWidth(200f) // placeholder for "Mod actions"
|
topTable.add("".toLabel()).minWidth(200f) // placeholder for "Mod actions"
|
||||||
topTable.add().expandX()
|
topTable.add().expandX()
|
||||||
topTable.row()
|
topTable.row()
|
||||||
@ -180,16 +167,23 @@ class ModManagementScreen: PickerScreen(disableScroll = true) {
|
|||||||
// main row containing the three 'blocks' installed, online and information
|
// main row containing the three 'blocks' installed, online and information
|
||||||
topTable.add() // skip empty first column
|
topTable.add() // skip empty first column
|
||||||
topTable.add(scrollInstalledMods)
|
topTable.add(scrollInstalledMods)
|
||||||
|
|
||||||
topTable.add(scrollOnlineMods)
|
topTable.add(scrollOnlineMods)
|
||||||
|
|
||||||
topTable.add(modActionTable)
|
topTable.add(modActionTable)
|
||||||
|
topTable.add().row()
|
||||||
|
topTable.add().expandY() // So short lists won't vertically center everything including headers
|
||||||
|
|
||||||
|
stage.addActor(optionsManager.expander)
|
||||||
|
optionsManager.expanderChangeEvent = {
|
||||||
|
optionsManager.expander.pack()
|
||||||
|
optionsManager.expander.setPosition(stage.width - 2f, stage.height - 2f, Align.topRight)
|
||||||
|
}
|
||||||
|
optionsManager.expanderChangeEvent?.invoke()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun reloadOnlineMods() {
|
private fun reloadOnlineMods() {
|
||||||
onlineScrollCurrentY = -1f
|
onlineScrollCurrentY = -1f
|
||||||
downloadTable.clear()
|
downloadTable.clear()
|
||||||
onlineScrollIndex.clear()
|
onlineModInfo.clear()
|
||||||
downloadTable.add(getDownloadFromUrlButton()).padBottom(15f).row()
|
downloadTable.add(getDownloadFromUrlButton()).padBottom(15f).row()
|
||||||
downloadTable.add("...".toLabel()).row()
|
downloadTable.add("...".toLabel()).row()
|
||||||
tryDownloadPage(1)
|
tryDownloadPage(1)
|
||||||
@ -213,11 +207,11 @@ class ModManagementScreen: PickerScreen(disableScroll = true) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Gdx.app.postRunnable {
|
Gdx.app.postRunnable {
|
||||||
// clear and hide last cell if it is the "..." indicator
|
// clear and remove last cell if it is the "..." indicator
|
||||||
val lastCell = downloadTable.cells.lastOrNull()
|
val lastCell = downloadTable.cells.lastOrNull()
|
||||||
if (lastCell != null && lastCell.actor is Label && (lastCell.actor as Label).text.toString() == "...") {
|
if (lastCell != null && lastCell.actor is Label && (lastCell.actor as Label).text.toString() == "...") {
|
||||||
lastCell.setActor<Actor>(null)
|
lastCell.setActor<Actor>(null)
|
||||||
lastCell.pad(0f)
|
downloadTable.cells.removeValue(lastCell, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
for (repo in repoSearch.items) {
|
for (repo in repoSearch.items) {
|
||||||
@ -227,18 +221,16 @@ class ModManagementScreen: PickerScreen(disableScroll = true) {
|
|||||||
// 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 modsToHideAsUrl) continue
|
if (repo.html_url in modsToHideAsUrl) continue
|
||||||
|
|
||||||
modDescriptionsOnline[repo.name] =
|
|
||||||
(repo.description ?: "-{No description provided}-".tr()) +
|
|
||||||
"\n" + "[${repo.stargazers_count}]✯".tr()
|
|
||||||
|
|
||||||
var downloadButtonText = repo.name
|
|
||||||
val existingMod = RulesetCache.values.firstOrNull { it.name == repo.name }
|
val existingMod = RulesetCache.values.firstOrNull { it.name == repo.name }
|
||||||
|
val isUpdated = existingMod?.modOptions?.let {
|
||||||
|
it.lastUpdated != "" && it.lastUpdated != repo.updated_at
|
||||||
|
} == true
|
||||||
|
|
||||||
if (existingMod != null) {
|
if (existingMod != null) {
|
||||||
if (existingMod.modOptions.lastUpdated != "" && existingMod.modOptions.lastUpdated != repo.updated_at) {
|
if (isUpdated) {
|
||||||
downloadButtonText += " - {Updated}"
|
installedModInfo[repo.name]?.state?.isUpdated = true
|
||||||
modStateImages[repo.name]?.isUpdated = true
|
|
||||||
}
|
}
|
||||||
if (existingMod.modOptions.author.isEmpty()) {
|
if (existingMod.modOptions.author.isEmpty()) {
|
||||||
rewriteModOptions(repo, Gdx.files.local("mods").child(repo.name))
|
rewriteModOptions(repo, Gdx.files.local("mods").child(repo.name))
|
||||||
@ -246,13 +238,16 @@ class ModManagementScreen: PickerScreen(disableScroll = true) {
|
|||||||
existingMod.modOptions.modSize = repo.size
|
existingMod.modOptions.modSize = repo.size
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val downloadButton = downloadButtonText.toTextButton()
|
|
||||||
downloadButton.onClick { onlineButtonAction(repo, downloadButton) }
|
|
||||||
|
|
||||||
val cell = downloadTable.add(downloadButton)
|
val mod = ModUIData(repo, isUpdated)
|
||||||
|
onlineModInfo[repo.name] = mod
|
||||||
|
mod.button.onClick { onlineButtonAction(repo, mod.button) }
|
||||||
|
|
||||||
|
val cell = downloadTable.add(mod.button)
|
||||||
downloadTable.row()
|
downloadTable.row()
|
||||||
if (onlineScrollCurrentY < 0f) onlineScrollCurrentY = cell.padTop
|
if (onlineScrollCurrentY < 0f) onlineScrollCurrentY = cell.padTop
|
||||||
onlineScrollIndex[repo.name] = ScrollToEntry(onlineScrollCurrentY, cell.prefHeight, downloadButton)
|
mod.y = onlineScrollCurrentY
|
||||||
|
mod.height = cell.prefHeight
|
||||||
onlineScrollCurrentY += cell.padBottom + cell.prefHeight + cell.padTop
|
onlineScrollCurrentY += cell.padBottom + cell.prefHeight + cell.padTop
|
||||||
downloadModCount++
|
downloadModCount++
|
||||||
}
|
}
|
||||||
@ -273,7 +268,7 @@ class ModManagementScreen: PickerScreen(disableScroll = true) {
|
|||||||
}
|
}
|
||||||
duplicates.forEach {
|
duplicates.forEach {
|
||||||
it.setActor(null)
|
it.setActor(null)
|
||||||
it.pad(0f) // the cell itself cannot be removed so stop it occupying height
|
downloadTable.cells.removeValue(it, true)
|
||||||
}
|
}
|
||||||
downloadModCount -= duplicates.size
|
downloadModCount -= duplicates.size
|
||||||
// Check: It is also not impossible we missed a mod - just inform user
|
// Check: It is also not impossible we missed a mod - just inform user
|
||||||
@ -303,12 +298,12 @@ class ModManagementScreen: PickerScreen(disableScroll = true) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun syncOnlineSelected(name: String, button: Button) {
|
private fun syncOnlineSelected(name: String, button: Button) {
|
||||||
syncSelected(name, button, installedScrollIndex, scrollInstalledMods)
|
syncSelected(name, button, installedModInfo, scrollInstalledMods)
|
||||||
}
|
}
|
||||||
private fun syncInstalledSelected(name: String, button: Button) {
|
private fun syncInstalledSelected(name: String, button: Button) {
|
||||||
syncSelected(name, button, onlineScrollIndex, scrollOnlineMods)
|
syncSelected(name, button, onlineModInfo, scrollOnlineMods)
|
||||||
}
|
}
|
||||||
private fun syncSelected(name: String, button: Button, index: HashMap<String, ScrollToEntry>, scroll: ScrollPane) {
|
private fun syncSelected(name: String, button: Button, index: HashMap<String, ModUIData>, scroll: ScrollPane) {
|
||||||
// manage selection color for user selection
|
// manage selection color for user selection
|
||||||
lastSelectedButton?.color = Color.WHITE
|
lastSelectedButton?.color = Color.WHITE
|
||||||
button.color = Color.BLUE
|
button.color = Color.BLUE
|
||||||
@ -391,7 +386,7 @@ class ModManagementScreen: PickerScreen(disableScroll = true) {
|
|||||||
showModDescription(repo.name)
|
showModDescription(repo.name)
|
||||||
removeRightSideClickListeners()
|
removeRightSideClickListeners()
|
||||||
rightSideButton.enable()
|
rightSideButton.enable()
|
||||||
val label = if (modStateImages[repo.name]?.isUpdated == true)
|
val label = if (installedModInfo[repo.name]?.state?.isUpdated == true)
|
||||||
"Update [${repo.name}]"
|
"Update [${repo.name}]"
|
||||||
else "Download [${repo.name}]"
|
else "Download [${repo.name}]"
|
||||||
rightSideButton.setText(label.tr())
|
rightSideButton.setText(label.tr())
|
||||||
@ -418,6 +413,9 @@ class ModManagementScreen: PickerScreen(disableScroll = true) {
|
|||||||
Gdx.app.postRunnable {
|
Gdx.app.postRunnable {
|
||||||
ToastPopup("[${repo.name}] Downloaded!", this)
|
ToastPopup("[${repo.name}] Downloaded!", this)
|
||||||
RulesetCache.loadRulesets()
|
RulesetCache.loadRulesets()
|
||||||
|
RulesetCache[repo.name]?.let {
|
||||||
|
installedModInfo[repo.name] = ModUIData(it)
|
||||||
|
}
|
||||||
refreshInstalledModTable()
|
refreshInstalledModTable()
|
||||||
showModDescription(repo.name)
|
showModDescription(repo.name)
|
||||||
unMarkUpdatedMod(repo.name)
|
unMarkUpdatedMod(repo.name)
|
||||||
@ -453,9 +451,14 @@ class ModManagementScreen: PickerScreen(disableScroll = true) {
|
|||||||
* (called under postRunnable posted by background thread)
|
* (called under postRunnable posted by background thread)
|
||||||
*/
|
*/
|
||||||
private fun unMarkUpdatedMod(name: String) {
|
private fun unMarkUpdatedMod(name: String) {
|
||||||
modStateImages[name]?.isUpdated = false
|
installedModInfo[name]?.state?.isUpdated = false
|
||||||
val button = (onlineScrollIndex[name]?.button as? TextButton) ?: return
|
onlineModInfo[name]?.state?.isUpdated = false
|
||||||
button.setText(name)
|
val button = (onlineModInfo[name]?.button as? TextButton)
|
||||||
|
button?.setText(name)
|
||||||
|
if (optionsManager.sortInstalled == SortType.Status)
|
||||||
|
refreshInstalledModTable()
|
||||||
|
if (optionsManager.sortOnline == SortType.Status)
|
||||||
|
refreshOnlineModTable()
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Rebuild the right-hand column for clicks on installed mods
|
/** Rebuild the right-hand column for clicks on installed mods
|
||||||
@ -469,7 +472,7 @@ class ModManagementScreen: PickerScreen(disableScroll = true) {
|
|||||||
// offer 'permanent visual mod' toggle
|
// offer 'permanent visual mod' toggle
|
||||||
val visualMods = game.settings.visualMods
|
val visualMods = game.settings.visualMods
|
||||||
val isVisual = visualMods.contains(mod.name)
|
val isVisual = visualMods.contains(mod.name)
|
||||||
modStateImages[mod.name]?.isVisual = isVisual
|
installedModInfo[mod.name]?.state?.isVisual = isVisual
|
||||||
|
|
||||||
val visualCheckBox = "Permanent audiovisual mod".toCheckBox(isVisual) {
|
val visualCheckBox = "Permanent audiovisual mod".toCheckBox(isVisual) {
|
||||||
checked ->
|
checked ->
|
||||||
@ -480,79 +483,121 @@ class ModManagementScreen: PickerScreen(disableScroll = true) {
|
|||||||
game.settings.save()
|
game.settings.save()
|
||||||
ImageGetter.setNewRuleset(ImageGetter.ruleset)
|
ImageGetter.setNewRuleset(ImageGetter.ruleset)
|
||||||
refreshModActions(mod)
|
refreshModActions(mod)
|
||||||
|
if (optionsManager.sortInstalled == SortType.Status)
|
||||||
|
refreshInstalledModTable()
|
||||||
}
|
}
|
||||||
modActionTable.add(visualCheckBox).row()
|
modActionTable.add(visualCheckBox).row()
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Rebuild the left-hand column containing all installed mods */
|
/** Rebuild the left-hand column containing all installed mods */
|
||||||
private fun refreshInstalledModTable() {
|
internal fun refreshInstalledModTable() {
|
||||||
modTable.clear()
|
// pre-init if not already done - important: keep the ModUIData instances later on or
|
||||||
installedScrollIndex.clear()
|
// at least the button references otherwise sync will not work
|
||||||
|
if (installedModInfo.isEmpty()) {
|
||||||
var currentY = -1f
|
for (mod in RulesetCache.values.asSequence().filter { it.name != "" }) {
|
||||||
val currentMods = RulesetCache.values.asSequence().filter { it.name != "" }.sortedBy { it.name }
|
ModUIData(mod).run {
|
||||||
for (mod in currentMods) {
|
state.isVisual = mod.name in game.settings.visualMods
|
||||||
val summary = mod.getSummary()
|
installedModInfo[mod.name] = this
|
||||||
modDescriptionsInstalled[mod.name] = "Installed".tr() +
|
|
||||||
(if (summary.isEmpty()) "" else ": $summary")
|
|
||||||
|
|
||||||
var imageMgr = modStateImages[mod.name]
|
|
||||||
val decorationTable =
|
|
||||||
if (imageMgr != null) imageMgr.container
|
|
||||||
else {
|
|
||||||
val table = Table().apply { defaults().size(20f).align(Align.topLeft) }
|
|
||||||
imageMgr = ModStateImages(table, isVisual = mod.name in game.settings.visualMods)
|
|
||||||
modStateImages[mod.name] = imageMgr
|
|
||||||
table
|
|
||||||
}
|
|
||||||
imageMgr.update() // rebuilds decorationTable content
|
|
||||||
|
|
||||||
val button = mod.name.toTextButton()
|
|
||||||
button.onClick {
|
|
||||||
syncInstalledSelected(mod.name, button)
|
|
||||||
refreshModActions(mod)
|
|
||||||
rightSideButton.setText("Delete [${mod.name}]".tr())
|
|
||||||
rightSideButton.isEnabled = true
|
|
||||||
showModDescription(mod.name)
|
|
||||||
removeRightSideClickListeners()
|
|
||||||
rightSideButton.onClick {
|
|
||||||
rightSideButton.isEnabled = false
|
|
||||||
YesNoPopup(
|
|
||||||
question = "Are you SURE you want to delete this mod?",
|
|
||||||
action = {
|
|
||||||
deleteMod(mod)
|
|
||||||
rightSideButton.setText("[${mod.name}] was deleted.".tr())
|
|
||||||
},
|
|
||||||
screen = this,
|
|
||||||
restoreDefault = { rightSideButton.isEnabled = true }
|
|
||||||
).open()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val newHeaderText = optionsManager.getInstalledHeader()
|
||||||
|
installedHeaderLabel?.setText(newHeaderText)
|
||||||
|
installedExpanderTab?.setText(newHeaderText)
|
||||||
|
|
||||||
|
modTable.clear()
|
||||||
|
var currentY = -1f
|
||||||
|
val filter = optionsManager.getFilterText()
|
||||||
|
for (mod in installedModInfo.values.sortedWith(optionsManager.sortInstalled.comparator)) {
|
||||||
|
if (!mod.matchesFilter(filter)) continue
|
||||||
|
// Prevent building up listeners. The virgin Button has one: for mouseover styling.
|
||||||
|
// The captures for our listener shouldn't need updating, so assign only once
|
||||||
|
if (mod.button.listeners.none { it.javaClass.`package`.name.startsWith("com.unciv") })
|
||||||
|
mod.button.onClick { installedButtonAction(mod) }
|
||||||
val decoratedButton = Table()
|
val decoratedButton = Table()
|
||||||
decoratedButton.add(button)
|
decoratedButton.add(mod.button)
|
||||||
decoratedButton.add(decorationTable).align(Align.center+Align.left)
|
decoratedButton.add(mod.state.container).align(Align.center+Align.left)
|
||||||
val cell = modTable.add(decoratedButton)
|
val cell = modTable.add(decoratedButton)
|
||||||
modTable.row()
|
modTable.row()
|
||||||
if (currentY < 0f) currentY = cell.padTop
|
if (currentY < 0f) currentY = cell.padTop
|
||||||
installedScrollIndex[mod.name] = ScrollToEntry(currentY, cell.prefHeight, button)
|
mod.y = currentY
|
||||||
|
mod.height = cell.prefHeight
|
||||||
currentY += cell.padBottom + cell.prefHeight + cell.padTop
|
currentY += cell.padBottom + cell.prefHeight + cell.padTop
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun installedButtonAction(mod: ModUIData) {
|
||||||
|
syncInstalledSelected(mod.name, mod.button)
|
||||||
|
refreshModActions(mod.ruleset!!)
|
||||||
|
rightSideButton.setText("Delete [${mod.name}]".tr())
|
||||||
|
rightSideButton.isEnabled = true
|
||||||
|
showModDescription(mod.name)
|
||||||
|
removeRightSideClickListeners()
|
||||||
|
rightSideButton.onClick {
|
||||||
|
rightSideButton.isEnabled = false
|
||||||
|
YesNoPopup(
|
||||||
|
question = "Are you SURE you want to delete this mod?",
|
||||||
|
action = {
|
||||||
|
deleteMod(mod.name)
|
||||||
|
rightSideButton.setText("[${mod.name}] was deleted.".tr())
|
||||||
|
},
|
||||||
|
screen = this,
|
||||||
|
restoreDefault = { rightSideButton.isEnabled = true }
|
||||||
|
).open()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Delete a Mod, refresh ruleset cache and update installed mod table */
|
/** Delete a Mod, refresh ruleset cache and update installed mod table */
|
||||||
private fun deleteMod(mod: Ruleset) {
|
private fun deleteMod(modName: String) {
|
||||||
val modFileHandle = Gdx.files.local("mods").child(mod.name)
|
val modFileHandle = Gdx.files.local("mods").child(modName)
|
||||||
if (modFileHandle.isDirectory) modFileHandle.deleteDirectory()
|
if (modFileHandle.isDirectory) modFileHandle.deleteDirectory()
|
||||||
else modFileHandle.delete() // This should never happen
|
else modFileHandle.delete() // This should never happen
|
||||||
RulesetCache.loadRulesets()
|
RulesetCache.loadRulesets()
|
||||||
modStateImages.remove(mod.name)
|
installedModInfo.remove(modName)
|
||||||
refreshInstalledModTable()
|
refreshInstalledModTable()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal fun refreshOnlineModTable() {
|
||||||
|
if (runningSearchThread != null) return // cowardice: prevent concurrent modification, avoid a manager layer
|
||||||
|
|
||||||
|
val newHeaderText = optionsManager.getOnlineHeader()
|
||||||
|
onlineHeaderLabel?.setText(newHeaderText)
|
||||||
|
onlineExpanderTab?.setText(newHeaderText)
|
||||||
|
|
||||||
|
downloadTable.clear()
|
||||||
|
onlineScrollCurrentY = -1f
|
||||||
|
|
||||||
|
val filter = optionsManager.getFilterText()
|
||||||
|
// Important: sortedMods holds references to the original values, so the referenced buttons stay valid.
|
||||||
|
// We update y and height here, we do not replace the ModUIData instances do the referenced buttons stay valid.
|
||||||
|
val sortedMods = onlineModInfo.values.asSequence().sortedWith(optionsManager.sortOnline.comparator)
|
||||||
|
for (mod in sortedMods) {
|
||||||
|
if (!mod.matchesFilter(filter)) continue
|
||||||
|
val cell = downloadTable.add(mod.button)
|
||||||
|
downloadTable.row()
|
||||||
|
if (onlineScrollCurrentY < 0f) onlineScrollCurrentY = cell.padTop
|
||||||
|
mod.y = onlineScrollCurrentY
|
||||||
|
mod.height = cell.prefHeight
|
||||||
|
onlineScrollCurrentY += cell.padBottom + cell.prefHeight + cell.padTop
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadTable.pack()
|
||||||
|
(downloadTable.parent as ScrollPane).actor = downloadTable
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showModDescription(modName: String) {
|
||||||
|
val online = onlineModInfo[modName]?.description ?: ""
|
||||||
|
val installed = installedModInfo[modName]?.description ?: ""
|
||||||
|
val separator = if (online.isEmpty() || installed.isEmpty()) "" else "\n"
|
||||||
|
deprecationCell.setActor(if (modName in modsToHideNames) deprecationLabel else null)
|
||||||
|
modDescriptionLabel.setText(online + separator + installed)
|
||||||
|
}
|
||||||
|
|
||||||
override fun resize(width: Int, height: Int) {
|
override fun resize(width: Int, height: Int) {
|
||||||
if (stage.viewport.screenWidth != width || stage.viewport.screenHeight != height) {
|
if (stage.viewport.screenWidth != width || stage.viewport.screenHeight != height) {
|
||||||
game.setScreen(ModManagementScreen())
|
game.setScreen(ModManagementScreen(installedModInfo, onlineModInfo))
|
||||||
|
dispose() // interrupt background loader - sorry, the resized new screen won't continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -567,5 +612,6 @@ class ModManagementScreen: PickerScreen(disableScroll = true) {
|
|||||||
regex.replace(url) { it.groups[1]!!.value }.replace('-', ' ')
|
regex.replace(url) { it.groups[1]!!.value }.replace('-', ' ')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const val deprecationText = "Deprecated until update conforms to current requirements"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -20,8 +20,8 @@ import kotlin.concurrent.thread
|
|||||||
|
|
||||||
open class CameraStageBaseScreen : Screen {
|
open class CameraStageBaseScreen : Screen {
|
||||||
|
|
||||||
var game: UncivGame = UncivGame.Current
|
val game: UncivGame = UncivGame.Current
|
||||||
var stage: Stage
|
val stage: Stage
|
||||||
|
|
||||||
protected val tutorialController by lazy { TutorialController(this) }
|
protected val tutorialController by lazy { TutorialController(this) }
|
||||||
|
|
||||||
@ -34,6 +34,12 @@ open class CameraStageBaseScreen : Screen {
|
|||||||
/** The ExtendViewport sets the _minimum_(!) world size - the actual world size will be larger, fitted to screen/window aspect ratio. */
|
/** The ExtendViewport sets the _minimum_(!) world size - the actual world size will be larger, fitted to screen/window aspect ratio. */
|
||||||
stage = Stage(ExtendViewport(height, height), SpriteBatch())
|
stage = Stage(ExtendViewport(height, height), SpriteBatch())
|
||||||
|
|
||||||
|
if (enableSceneDebug) {
|
||||||
|
stage.setDebugUnderMouse(true)
|
||||||
|
stage.setDebugTableUnderMouse(true)
|
||||||
|
stage.setDebugParentUnderMouse(true)
|
||||||
|
}
|
||||||
|
|
||||||
keyPressDispatcher.install(stage) { hasOpenPopups() }
|
keyPressDispatcher.install(stage) { hasOpenPopups() }
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -69,7 +75,9 @@ open class CameraStageBaseScreen : Screen {
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
lateinit var skin:Skin
|
var enableSceneDebug = false
|
||||||
|
|
||||||
|
lateinit var skin: Skin
|
||||||
fun setSkin() {
|
fun setSkin() {
|
||||||
Fonts.resetFont()
|
Fonts.resetFont()
|
||||||
skin = Skin().apply {
|
skin = Skin().apply {
|
||||||
@ -93,7 +101,6 @@ open class CameraStageBaseScreen : Screen {
|
|||||||
skin.get(TextField.TextFieldStyle::class.java).font = Fonts.font.apply { data.setScale(18 / Fonts.ORIGINAL_FONT_SIZE) }
|
skin.get(TextField.TextFieldStyle::class.java).font = Fonts.font.apply { data.setScale(18 / Fonts.ORIGINAL_FONT_SIZE) }
|
||||||
skin.get(SelectBox.SelectBoxStyle::class.java).font = Fonts.font.apply { data.setScale(20 / Fonts.ORIGINAL_FONT_SIZE) }
|
skin.get(SelectBox.SelectBoxStyle::class.java).font = Fonts.font.apply { data.setScale(20 / Fonts.ORIGINAL_FONT_SIZE) }
|
||||||
skin.get(SelectBox.SelectBoxStyle::class.java).listStyle.font = Fonts.font.apply { data.setScale(20 / Fonts.ORIGINAL_FONT_SIZE) }
|
skin.get(SelectBox.SelectBoxStyle::class.java).listStyle.font = Fonts.font.apply { data.setScale(20 / Fonts.ORIGINAL_FONT_SIZE) }
|
||||||
skin
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,7 +15,8 @@ import com.unciv.UncivGame
|
|||||||
* @param title The header text, automatically translated.
|
* @param title The header text, automatically translated.
|
||||||
* @param fontSize Size applied to header text (only)
|
* @param fontSize Size applied to header text (only)
|
||||||
* @param icon Optional icon - please use [Image][com.badlogic.gdx.scenes.scene2d.ui.Image] or [IconCircleGroup]
|
* @param icon Optional icon - please use [Image][com.badlogic.gdx.scenes.scene2d.ui.Image] or [IconCircleGroup]
|
||||||
* @param defaultPad Padding between content and wrapper. Header padding is currently not modifiable.
|
* @param defaultPad Padding between content and wrapper.
|
||||||
|
* @param headerPad Default padding for the header Table.
|
||||||
* @param expanderWidth If set initializes header width
|
* @param expanderWidth If set initializes header width
|
||||||
* @param persistenceID If specified, the ExpanderTab will remember its open/closed state for the duration of one app run
|
* @param persistenceID If specified, the ExpanderTab will remember its open/closed state for the duration of one app run
|
||||||
* @param onChange If specified, this will be called after the visual change for a change in [isOpen] completes (e.g. to react to changed size)
|
* @param onChange If specified, this will be called after the visual change for a change in [isOpen] completes (e.g. to react to changed size)
|
||||||
@ -27,6 +28,7 @@ class ExpanderTab(
|
|||||||
icon: Actor? = null,
|
icon: Actor? = null,
|
||||||
startsOutOpened: Boolean = true,
|
startsOutOpened: Boolean = true,
|
||||||
defaultPad: Float = 10f,
|
defaultPad: Float = 10f,
|
||||||
|
headerPad: Float = 10f,
|
||||||
expanderWidth: Float = 0f,
|
expanderWidth: Float = 0f,
|
||||||
private val persistenceID: String? = null,
|
private val persistenceID: String? = null,
|
||||||
private val onChange: (() -> Unit)? = null,
|
private val onChange: (() -> Unit)? = null,
|
||||||
@ -37,7 +39,7 @@ class ExpanderTab(
|
|||||||
const val arrowImage = "OtherIcons/BackArrow"
|
const val arrowImage = "OtherIcons/BackArrow"
|
||||||
val arrowColor = Color(1f,0.96f,0.75f,1f)
|
val arrowColor = Color(1f,0.96f,0.75f,1f)
|
||||||
const val animationDuration = 0.2f
|
const val animationDuration = 0.2f
|
||||||
|
|
||||||
val persistedStates = HashMap<String, Boolean>()
|
val persistedStates = HashMap<String, Boolean>()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -59,7 +61,7 @@ class ExpanderTab(
|
|||||||
}
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
header.defaults().pad(10f)
|
header.defaults().pad(headerPad)
|
||||||
headerIcon.setSize(arrowSize, arrowSize)
|
headerIcon.setSize(arrowSize, arrowSize)
|
||||||
headerIcon.setOrigin(Align.center)
|
headerIcon.setOrigin(Align.center)
|
||||||
headerIcon.rotation = 180f
|
headerIcon.rotation = 180f
|
||||||
@ -115,4 +117,9 @@ class ExpanderTab(
|
|||||||
fun toggle() {
|
fun toggle() {
|
||||||
isOpen = !isOpen
|
isOpen = !isOpen
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Change header label text after initialization */
|
||||||
|
fun setText(text: String) {
|
||||||
|
headerLabel.setText(text)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -318,6 +318,9 @@ class OptionsPopup(val previousScreen: CameraStageBaseScreen) : Popup(previousSc
|
|||||||
add("Save maps compressed".toCheckBox(MapSaver.saveZipped) {
|
add("Save maps compressed".toCheckBox(MapSaver.saveZipped) {
|
||||||
MapSaver.saveZipped = it
|
MapSaver.saveZipped = it
|
||||||
}).row()
|
}).row()
|
||||||
|
add("Gdx Scene2D debug".toCheckBox(CameraStageBaseScreen.enableSceneDebug) {
|
||||||
|
CameraStageBaseScreen.enableSceneDebug = it
|
||||||
|
}).row()
|
||||||
}
|
}
|
||||||
|
|
||||||
//endregion
|
//endregion
|
||||||
|
@ -638,6 +638,7 @@ Unless otherwise specified, all the following are from [the Noun Project](https:
|
|||||||
* [connection](https://thenounproject.com/term/connection/1365233/) by Popular for Mercantile City-States
|
* [connection](https://thenounproject.com/term/connection/1365233/) by Popular for Mercantile City-States
|
||||||
* [crossed sword](https://thenounproject.com/term/crossed-sword/2427559/) by ProSymbols for Militaristic City-States
|
* [crossed sword](https://thenounproject.com/term/crossed-sword/2427559/) by ProSymbols for Militaristic City-States
|
||||||
* [ship helm](https://thenounproject.com/term/ship-helm/2170591/) by Vectors Market for Maritime City-States
|
* [ship helm](https://thenounproject.com/term/ship-helm/2170591/) by Vectors Market for Maritime City-States
|
||||||
|
* [Magnifying Glass](https://thenounproject.com/term/magnifying-glass/1311/) by John Caserta for Mod filter
|
||||||
|
|
||||||
## Main menu
|
## Main menu
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user