Skip to content
This repository was archived by the owner on Dec 30, 2022. It is now read-only.

Commit 9327046

Browse files
committed
Better mod browsing by making categories multiselectable
Closes #79 Signed-off-by: deathsgun <deathsgun@protonmail.com>
1 parent 63747ab commit 9327046

File tree

11 files changed

+274
-67
lines changed

11 files changed

+274
-67
lines changed

src/main/kotlin/xyz/deathsgun/modmanager/api/gui/list/IListScreen.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,6 @@ interface IListScreen {
2424

2525
fun <E> updateSelectedEntry(widget: Any, entry: E?)
2626

27+
fun <E> updateMultipleEntries(widget: Any, entries: ArrayList<E>)
28+
2729
}
Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
/*
2+
* Copyright 2021 DeathsGun
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package xyz.deathsgun.modmanager.api.gui.list
18+
19+
import com.mojang.blaze3d.systems.RenderSystem
20+
import net.minecraft.client.MinecraftClient
21+
import net.minecraft.client.gui.screen.narration.NarrationMessageBuilder
22+
import net.minecraft.client.gui.widget.AlwaysSelectedEntryListWidget
23+
import net.minecraft.client.render.GameRenderer
24+
import net.minecraft.client.render.Tessellator
25+
import net.minecraft.client.render.VertexFormat
26+
import net.minecraft.client.render.VertexFormats
27+
import net.minecraft.client.util.math.MatrixStack
28+
import net.minecraft.util.math.MathHelper
29+
import java.util.*
30+
import kotlin.math.max
31+
32+
/**
33+
* Using ModMenu's implementation because the implementation from
34+
* Mojang is broken. [com.terraformersmc.modmenu.gui.ModsScreen].
35+
* All credits for this code go to the Terraformer's
36+
*/
37+
abstract class MultiSelectListWidget<E : MultiSelectListWidget.Entry<E>>(
38+
client: MinecraftClient,
39+
width: Int,
40+
height: Int,
41+
top: Int,
42+
var bottom: Int,
43+
itemHeight: Int,
44+
private val parent: IListScreen
45+
) : AlwaysSelectedEntryListWidget<E>(client, width, height, top, bottom, itemHeight) {
46+
47+
var renderOutline: Boolean = true
48+
private var selectedIds = ArrayList<String>()
49+
private var scrolling = false
50+
51+
abstract fun init()
52+
53+
override fun isFocused(): Boolean {
54+
return parent.getFocused() == this
55+
}
56+
57+
override fun setSelected(entry: E?) {
58+
super.setSelected(entry)
59+
if (entry == null) {
60+
return
61+
}
62+
if (selectedIds.contains(entry.id)) {
63+
selectedIds.removeIf { it == entry.id }
64+
} else {
65+
selectedIds.add(entry.id)
66+
}
67+
parent.updateMultipleEntries(this, ArrayList(children().filter { selectedIds.contains(it.id) }))
68+
}
69+
70+
fun setSelected(entries: List<E>) {
71+
selectedIds.clear()
72+
for (entry in entries) {
73+
setSelected(entry)
74+
}
75+
}
76+
77+
override fun isSelectedEntry(index: Int): Boolean {
78+
return selectedIds.contains(getEntry(index).id)
79+
}
80+
81+
override fun addEntry(entry: E): Int {
82+
val i = super.addEntry(entry)
83+
if (selectedIds.contains(entry.id)) {
84+
setSelected(entry)
85+
}
86+
return i
87+
}
88+
89+
override fun renderList(matrices: MatrixStack?, x: Int, y: Int, mouseX: Int, mouseY: Int, delta: Float) {
90+
val itemCount = this.entryCount
91+
val tessellator = Tessellator.getInstance()
92+
val buffer = tessellator.buffer
93+
94+
for (index in 0 until itemCount) {
95+
val entryTop = getRowTop(index) + 2
96+
val entryBottom = getRowTop(index) + itemHeight
97+
if (entryBottom >= top && entryTop <= bottom) {
98+
val entryHeight = itemHeight - 4
99+
val entry: Entry<E> = getEntry(index)
100+
val rowWidth = this.rowWidth
101+
var entryLeft: Int
102+
if (isSelectedEntry(index) && renderOutline) {
103+
entryLeft = rowLeft - 2
104+
val selectionRight = x + rowWidth + 2
105+
RenderSystem.disableTexture()
106+
val color = if (this.isFocused) 1.0f else 0.5f
107+
RenderSystem.setShader { GameRenderer.getPositionShader() }
108+
RenderSystem.setShaderColor(color, color, color, 1.0f)
109+
val matrix = matrices!!.peek().model
110+
buffer.begin(VertexFormat.DrawMode.QUADS, VertexFormats.POSITION)
111+
buffer.vertex(matrix, entryLeft.toFloat(), (entryTop + entryHeight + 2).toFloat(), 0.0f).next()
112+
buffer.vertex(matrix, selectionRight.toFloat(), (entryTop + entryHeight + 2).toFloat(), 0.0f).next()
113+
buffer.vertex(matrix, selectionRight.toFloat(), (entryTop - 2).toFloat(), 0.0f).next()
114+
buffer.vertex(matrix, entryLeft.toFloat(), (entryTop - 2).toFloat(), 0.0f).next()
115+
tessellator.draw()
116+
RenderSystem.setShader { GameRenderer.getPositionShader() }
117+
RenderSystem.setShaderColor(0.0f, 0.0f, 0.0f, 1.0f)
118+
buffer.begin(VertexFormat.DrawMode.QUADS, VertexFormats.POSITION)
119+
buffer.vertex(matrix, (entryLeft + 1).toFloat(), (entryTop + entryHeight + 1).toFloat(), 0.0f)
120+
.next()
121+
buffer.vertex(matrix, (selectionRight - 1).toFloat(), (entryTop + entryHeight + 1).toFloat(), 0.0f)
122+
.next()
123+
buffer.vertex(matrix, (selectionRight - 1).toFloat(), (entryTop - 1).toFloat(), 0.0f).next()
124+
buffer.vertex(matrix, (entryLeft + 1).toFloat(), (entryTop - 1).toFloat(), 0.0f).next()
125+
tessellator.draw()
126+
RenderSystem.enableTexture()
127+
}
128+
entryLeft = this.rowLeft
129+
entry.render(
130+
matrices, index, entryTop, entryLeft, rowWidth, entryHeight, mouseX, mouseY, this.isMouseOver(
131+
mouseX.toDouble(), mouseY.toDouble()
132+
) && Objects.equals(this.getEntryAtPos(mouseX, mouseY), entry), delta
133+
)
134+
}
135+
}
136+
}
137+
138+
override fun getRowWidth(): Int {
139+
return width - if (max(0, this.maxPosition - (bottom - top - 4)) > 0) 18 else 12
140+
}
141+
142+
override fun getRowLeft(): Int {
143+
return left + 6
144+
}
145+
146+
override fun getScrollbarPositionX(): Int {
147+
return left + width - 6
148+
}
149+
150+
override fun getMaxPosition(): Int {
151+
return super.getMaxPosition() + 4
152+
}
153+
154+
override fun appendNarrations(builder: NarrationMessageBuilder?) {
155+
super.appendNarrations(builder)
156+
}
157+
158+
fun isSelectedEntry(entry: Entry<E>): Boolean {
159+
return selectedIds.contains(entry.id)
160+
}
161+
162+
fun getEntryAtPos(x: Int, y: Int): Entry<E>? {
163+
val int5 = MathHelper.floor(y - top.toDouble()) - headerHeight + scrollAmount.toInt() - 4
164+
val index = int5 / itemHeight
165+
return if (x < this.scrollbarPositionX.toDouble() && x >= rowLeft.toDouble() && x <= (rowLeft + rowWidth).toDouble() && index >= 0
166+
&& int5 >= 0 && index < this.entryCount
167+
) children()[index] else null
168+
}
169+
170+
fun getElementCount(): Int {
171+
return super.getEntryCount()
172+
}
173+
174+
public override fun remove(index: Int): E? {
175+
return super.remove(index)
176+
}
177+
178+
public override fun getEntry(index: Int): E {
179+
return super.getEntry(index)
180+
}
181+
182+
abstract class Entry<E : Entry<E>>(open val list: MultiSelectListWidget<E>, val id: String) :
183+
AlwaysSelectedEntryListWidget.Entry<E>() {
184+
@Suppress("UNCHECKED_CAST")
185+
override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
186+
list.setSelected(this as E)
187+
return true
188+
}
189+
}
190+
}

src/main/kotlin/xyz/deathsgun/modmanager/api/provider/IModProvider.kt

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,22 +45,24 @@ interface IModProvider : IModUpdateProvider {
4545
/**
4646
* Returns a limited number of [Mod]'s from the specified category
4747
*
48-
* @param category the category of all the mods
49-
* @param page the requested from the UI starting at 1
50-
* @param limit to not overfill the ui and for shorter loading times the amount of returned mods needs to limited
48+
* @param categories the categories of the mods
49+
* @param sorting the sorting order
50+
* @param page the requested from the UI starting at 1
51+
* @param limit to not overfill the ui and for shorter loading times the amount of returned mods needs to limited
5152
* @return a list of sorted mods
5253
*/
53-
fun getMods(category: Category, sorting: Sorting, page: Int, limit: Int): ModsResult
54+
fun getMods(categories: List<Category>, sorting: Sorting, page: Int, limit: Int): ModsResult
5455

5556
/**
5657
* Returns a limited number of [Mod]'s from a given search.
5758
*
5859
* @param query the search string
60+
* @param categories the categories in which should be searched
5961
* @param page the current requested page starts at 0
6062
* @param limit the amount of mods to return
6163
* @return a list of mods matching the search term
6264
*/
63-
fun search(query: String, sorting: Sorting, page: Int, limit: Int): ModsResult
65+
fun search(query: String, categories: List<Category>, sorting: Sorting, page: Int, limit: Int): ModsResult
6466

6567
/**
6668
* Returns a more detailed representation of the mod

src/main/kotlin/xyz/deathsgun/modmanager/gui/ModDetailScreen.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,4 +185,7 @@ class ModDetailScreen(private val previousScreen: Screen, var mod: Mod) : Screen
185185
override fun <E> updateSelectedEntry(widget: Any, entry: E?) {
186186
}
187187

188+
override fun <E> updateMultipleEntries(widget: Any, entries: ArrayList<E>) {
189+
}
190+
188191
}

src/main/kotlin/xyz/deathsgun/modmanager/gui/ModUpdateInfoScreen.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,4 +142,7 @@ class ModUpdateInfoScreen(private val previousScreen: Screen, private val update
142142
override fun <E> updateSelectedEntry(widget: Any, entry: E?) {
143143
}
144144

145+
override fun <E> updateMultipleEntries(widget: Any, entries: ArrayList<E>) {
146+
}
147+
145148
}

src/main/kotlin/xyz/deathsgun/modmanager/gui/ModsOverviewScreen.kt

Lines changed: 35 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ class ModsOverviewScreen(private val previousScreen: Screen) : Screen(Translatab
4646

4747
private var query: String = ""
4848
private var selectedMod: ModListEntry? = null
49-
private var selectedCategory: CategoryListEntry? = null
49+
private var selectedCategories: ArrayList<CategoryListEntry> = ArrayList()
5050
private var page: Int = 0
5151
private var limit: Int = 20
5252
private var scrollPercentage: Double = 0.0
@@ -137,13 +137,13 @@ class ModsOverviewScreen(private val previousScreen: Screen) : Screen(Translatab
137137
modList.scrollAmount = scrollPercentage
138138
}
139139
}
140-
if (selectedCategory != null) {
141-
if (selectedCategory!!.id == "updatable" && ModManager.modManager.update.updates.isEmpty()) {
140+
if (selectedCategories.isNotEmpty()) {
141+
if (selectedCategories.any { it.category.id == "updatable" } && ModManager.modManager.update.updates.isEmpty()) {
142142
categoryList.setSelectedByIndex(0)
143143
showModsByCategory()
144144
return@launch
145145
}
146-
categoryList.setSelected(selectedCategory)
146+
categoryList.setSelected(selectedCategories)
147147
showModsByCategory()
148148
modList.scrollAmount = scrollPercentage
149149
return@launch
@@ -152,7 +152,7 @@ class ModsOverviewScreen(private val previousScreen: Screen) : Screen(Translatab
152152
showModsBySearch()
153153
return@launch
154154
}
155-
categoryList.setSelectedByIndex(0)
155+
showModsByCategory()
156156
}
157157
modList.init()
158158
categoryList.init()
@@ -178,38 +178,20 @@ class ModsOverviewScreen(private val previousScreen: Screen) : Screen(Translatab
178178
showModsBySearch()
179179
return
180180
}
181-
if (selectedCategory == null) {
182-
showModsByRelevance()
183-
return
184-
}
185181
showModsByCategory()
186182
}
187183

188-
private fun showModsByRelevance() {
189-
val provider = ModManager.modManager.getSelectedProvider() ?: return
190-
when (val result = provider.getMods(Sorting.RELEVANCE, page, limit)) {
191-
is ModsResult.Error -> {
192-
error = result.text
193-
}
194-
is ModsResult.Success -> {
195-
error = null
196-
modList.setMods(result.mods)
197-
}
198-
}
199-
}
200-
201184
private fun showModsByCategory() {
202-
selectedCategory ?: return
203185
query = ""
204186
val provider = ModManager.modManager.getSelectedProvider() ?: return
205-
if (selectedCategory!!.id == "updatable") {
187+
if (selectedCategories.any { it.id == "updatable" }) {
206188
modList.clear()
207189
ModManager.modManager.update.updates.forEach {
208190
modList.add(it.mod)
209191
}
210192
return
211193
}
212-
when (val result = provider.getMods(selectedCategory!!.category, sorting, page, limit)) {
194+
when (val result = provider.getMods(selectedCategories.map { it.category }, sorting, page, limit)) {
213195
is ModsResult.Error -> {
214196
error = result.text
215197
}
@@ -232,7 +214,7 @@ class ModsOverviewScreen(private val previousScreen: Screen) : Screen(Translatab
232214

233215
private fun showModsBySearch() {
234216
val provider = ModManager.modManager.getSelectedProvider() ?: return
235-
when (val result = provider.search(this.query, sorting, page, limit)) {
217+
when (val result = provider.search(this.query, selectedCategories.map { it.category }, sorting, page, limit)) {
236218
is ModsResult.Error -> {
237219
this.error = result.text
238220
}
@@ -248,32 +230,38 @@ class ModsOverviewScreen(private val previousScreen: Screen) : Screen(Translatab
248230
}
249231

250232
override fun <E> updateSelectedEntry(widget: Any, entry: E?) {
251-
if (widget is ModListWidget) {
252-
if (entry == null) {
253-
return
254-
}
255-
if (selectedMod == entry) {
256-
if (selectedCategory?.id == "updatable" && query.isEmpty()) {
257-
val update = ModManager.modManager.update.getUpdateForMod(selectedMod!!.mod) ?: return
258-
client?.setScreen(ModUpdateInfoScreen(this, update))
259-
return
260-
}
261-
client?.setScreen(ModDetailScreen(this, selectedMod!!.mod))
262-
return
263-
}
264-
selectedMod = entry as ModListEntry
233+
if (widget !is ModListWidget) {
234+
return
235+
}
236+
if (entry == null) {
265237
return
266238
}
267-
if (widget is CategoryListWidget) {
268-
if (entry == null) {
239+
if (selectedMod == entry) {
240+
if (selectedCategories.any { it.id == "updatable" } && query.isEmpty()) {
241+
val update = ModManager.modManager.update.getUpdateForMod(selectedMod!!.mod) ?: return
242+
client?.setScreen(ModUpdateInfoScreen(this, update))
269243
return
270244
}
271-
modList.scrollAmount = 0.0
272-
page = 0
273-
selectedCategory = entry as CategoryListEntry
274-
query = ""
275-
showModsByCategory()
245+
client?.setScreen(ModDetailScreen(this, selectedMod!!.mod))
246+
return
276247
}
248+
selectedMod = entry as ModListEntry
249+
}
250+
251+
override fun <E> updateMultipleEntries(widget: Any, entries: ArrayList<E>) {
252+
if (widget !is CategoryListWidget) {
253+
return
254+
}
255+
modList.scrollAmount = 0.0
256+
page = 0
257+
@Suppress("UNCHECKED_CAST")
258+
selectedCategories = entries as ArrayList<CategoryListEntry>
259+
if (selectedCategories.any { it.category.id == "updatable" } && selectedCategories.size > 1) {
260+
selectedCategories.removeIf { it.category.id != "updatable" }
261+
categoryList.setSelected(selectedCategories)
262+
}
263+
query = ""
264+
showModsByCategory()
277265
}
278266

279267
override fun tick() {

0 commit comments

Comments
 (0)