Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add toggle button to show/hide child mods, and a search widget #20

Closed
wants to merge 10 commits into from
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
# Changelog

## 2.0.0 - 2024-04-28

### Added

- Add a toggleable button for showing or hiding indirect mods. By default
indirect mods are not shown. Indirect mods are installed as child mods from a
top-level mod, and most of the time the setting screens are not relevant for
those.
- Add a search/filter widget to hide non-matching mods. This is useful when you
have a lot of mods installed. By typing a few characters, only mods with a
matching name or id will be shown.

## 1.2.0 - 2024-04-28

### Fixed
Expand Down
8 changes: 7 additions & 1 deletion src/main/java/se/icus/mag/modsettings/Main.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.lwjgl.glfw.GLFW;
import se.icus.mag.modsettings.gui.ModSettingsScreen;
import se.icus.mag.modsettings.gui.screen.ModSettingsScreen;

public class Main implements ClientModInitializer {
public static final Logger LOGGER = LogManager.getLogger("modsettings");
public static final Options OPTIONS = new Options();

@Override
public void onInitializeClient() {
Expand All @@ -25,4 +26,9 @@ public void onInitializeClient() {
}
});
}

public static class Options {
public String filterText = "";
public boolean showIndirect = false;
}
}
42 changes: 37 additions & 5 deletions src/main/java/se/icus/mag/modsettings/ModRegistry.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,27 @@
import com.terraformersmc.modmenu.api.ModMenuApi;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import net.fabricmc.loader.api.EntrypointException;
import net.fabricmc.loader.api.FabricLoader;
import net.fabricmc.loader.api.ModContainer;
import net.fabricmc.loader.api.entrypoint.EntrypointContainer;
import net.fabricmc.loader.api.metadata.ModMetadata;
import net.minecraft.client.gui.screen.Screen;
import org.apache.logging.log4j.Level;

public class ModRegistry {
private static final ModRegistry INSTANCE = new ModRegistry();

private final Map<String, String> modNames = new HashMap<>();
private final Map<String, Set<String>> modHierarchy = new HashMap<>();

private final Map<String, ConfigScreenFactory<?>> configScreenFactories = new HashMap<>();
private final Map<String, ConfigScreenFactory<?>> overridingConfigScreenFactories = new HashMap<>();

Expand All @@ -33,6 +37,18 @@ public static ModRegistry getInstance() {

/* This needs to be done att the right time of loading the mod, so cannot be done in the constructor. */
public void registerMods() {
for (ModContainer modContainer : FabricLoader.getInstance().getAllMods()) {
String modId = modContainer.getMetadata().getId();
Optional<ModContainer> parent = modContainer.getContainingMod();
if (parent.isPresent()) {
String parentId = parent.get().getMetadata().getId();
Set<String> parentMod = modHierarchy.computeIfAbsent(parentId, k -> new HashSet<>());
parentMod.add(modId);
} else {
modHierarchy.computeIfAbsent(modId, k -> new HashSet<>());
}
}

List<EntrypointContainer<Object>> modList =
FabricLoader.getInstance().getEntrypointContainers("modmenu", Object.class);

Expand All @@ -45,7 +61,7 @@ public void registerMods() {
ModMenuApi modApi;

if (unknownApi instanceof com.terraformersmc.modmenu.api.ModMenuApi modernApi) {
Main.LOGGER.log(Level.INFO,"Found configurable mod: " + modId + ", " + metadata.getName());
Main.LOGGER.info("Found configurable mod: " + modId + ", " + metadata.getName());
modApi = modernApi;
} else {
Main.LOGGER.warn("Unknown Mod Menu API version for mod " + modId + ", class: " + unknownApi.getClass());
Expand All @@ -62,7 +78,7 @@ public void registerMods() {
Optional<ModContainer> container = FabricLoader.getInstance().getModContainer(overriddenModId);
if (container.isPresent()) {
String modName = container.get().getMetadata().getName();
Main.LOGGER.log(Level.INFO, "Found overridden config for mod: " + overriddenModId + ", " + modName);
Main.LOGGER.info("Found overridden config for mod: " + overriddenModId + ", " + modName);

modNames.put(overriddenModId, modName);
}
Expand All @@ -73,14 +89,30 @@ public void registerMods() {
}
}

public List<String> getAllModIds() {
public Stream<String> getAllModIds() {
// Return mods sorted. This sorts on modID and not name, but is good enough.
Comparator<String> sorter = Comparator.comparing(modId -> modId.toLowerCase(Locale.ROOT));

// Fabric treats Vanilla ("minecraft") as a mod and returns the normal Options screen.
// We don't want that so filter it out.
return modNames.keySet().stream().sorted(sorter)
.filter(modId -> !modId.equals("minecraft")).collect(Collectors.toList());
.filter(modId -> !modId.equals("minecraft"));
}

public List<String> getVisibleModIds(boolean showIndirect, String filterText) {
// If showIndirect is false, only include mods that is a parent.
return getAllModIds()
.filter(modId -> showIndirect || modHierarchy.containsKey(modId))
.filter(modId -> filterText.isBlank() || modIdMatches(modId, filterText))
.collect(Collectors.toList());
}

private boolean matches(String haystack, String needle) {
return haystack.toLowerCase(Locale.ROOT).contains(needle.toLowerCase(Locale.ROOT));
}

private boolean modIdMatches(String modId, String filter) {
return matches(modId, filter) || matches(getModName(modId), filter);
}

public String getModName(String modId) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
import net.minecraft.client.gui.screen.Screen;
import net.minecraft.client.gui.widget.ClickableWidget;
import net.minecraft.text.Text;
import se.icus.mag.modsettings.gui.screen.ModSettingsScreen;
import se.icus.mag.modsettings.gui.widget.Button;

public abstract class MenuScreensChanger {
private static final int TITLE_FULL_BUTTON_WIDTH = 200;
Expand Down
6 changes: 6 additions & 0 deletions src/main/java/se/icus/mag/modsettings/gui/ModConfigInfo.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package se.icus.mag.modsettings.gui;

import net.minecraft.client.gui.screen.Screen;

public record ModConfigInfo(String modId, String modName, Screen configScreen) {
}
74 changes: 0 additions & 74 deletions src/main/java/se/icus/mag/modsettings/gui/ModSettingsScreen.java

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package se.icus.mag.modsettings.gui.screen;

import java.util.LinkedList;
import java.util.List;
import net.minecraft.client.gui.screen.Screen;
import net.minecraft.client.gui.tooltip.Tooltip;
import net.minecraft.screen.ScreenTexts;
import net.minecraft.text.Text;
import net.minecraft.util.Identifier;
import se.icus.mag.modsettings.Main;
import se.icus.mag.modsettings.ModRegistry;
import se.icus.mag.modsettings.gui.ModConfigInfo;
import se.icus.mag.modsettings.gui.widget.Button;
import se.icus.mag.modsettings.gui.widget.IconToggleButtonWidget;
import se.icus.mag.modsettings.gui.widget.ModListWidget;
import se.icus.mag.modsettings.gui.widget.SearchWidget;

public class ModSettingsScreen extends TitledScreen {
private static final int FULL_BUTTON_WIDTH = 200;
private static final int BUTTON_HEIGHT = 20;

private boolean initIsProcessing;
private ModListWidget list;
private SearchWidget searchWidget;

public ModSettingsScreen(Screen previous) {
super(Text.translatable("modsettings.screen.title"), previous);
}

@Override
protected void init() {
// Protect against mods like Content Creator Integration that triggers
// a recursive call of Screen.init() while creating the settings screen...
if (initIsProcessing) return;
initIsProcessing = true;

// Add the toggle show indirect mods button
IconToggleButtonWidget showIndirectButton = new IconToggleButtonWidget(10, 6,
BUTTON_HEIGHT, BUTTON_HEIGHT, 15, 15,
List.of(new Identifier("modsettings", "expand"),
new Identifier("modsettings", "collapse")),
List.of(Tooltip.of(Text.translatable("modsettings.indirect.show")),
Tooltip.of(Text.translatable("modsettings.indirect.hide"))),
Main.OPTIONS.showIndirect ? 1 : 0, selection -> {
Main.OPTIONS.showIndirect = (selection == 1);
updateModButtons();
});
this.addDrawableChild(showIndirectButton);

// Add the search widget
searchWidget = new SearchWidget(40, 6, 100,
Main.OPTIONS.filterText, this.textRenderer, text -> {
Main.OPTIONS.filterText = text;
updateModButtons();
}, () -> this.setFocused(searchWidget));

this.addDrawableChild(searchWidget);
this.setInitialFocus(searchWidget);

// Add the actual mod list buttons
// Put the list between 32 pixels from top and bottom
this.list = new ModListWidget(this.client, this.width, this.height - 64, 32, 25);

this.addDrawableChild(this.list);

// Add the Done button
this.addDrawableChild(new Button(this.width / 2 - FULL_BUTTON_WIDTH / 2, this.height - 27, FULL_BUTTON_WIDTH, BUTTON_HEIGHT, ScreenTexts.DONE, button -> this.client.setScreen(this.previous)));

updateModButtons();
initIsProcessing = false;
}

private void updateModButtons() {
List<String> visibleModIds = ModRegistry.getInstance().getVisibleModIds(Main.OPTIONS.showIndirect, Main.OPTIONS.filterText);
this.list.setModButtons(getModConfigInfo(visibleModIds));
}

private List<ModConfigInfo> getModConfigInfo(List<String> modIds) {
List<ModConfigInfo> options = new LinkedList<>();
for (String modId : modIds) {
try {
Screen configScreen = ModRegistry.getInstance().getConfigScreen(modId, this);
if (configScreen != null) {
options.add(new ModConfigInfo(modId, ModRegistry.getInstance().getModName(modId), configScreen));
}
} catch (Throwable e) {
Main.LOGGER.error("Error creating Settings screen from mod " + modId, e);
}
}
return options;
}
}
26 changes: 26 additions & 0 deletions src/main/java/se/icus/mag/modsettings/gui/screen/TitledScreen.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package se.icus.mag.modsettings.gui.screen;

import net.minecraft.client.gui.DrawContext;
import net.minecraft.client.gui.screen.Screen;
import net.minecraft.text.Text;

public class TitledScreen extends Screen {
private static final int TITLE_COLOR = 0xffffff;
protected final Screen previous;

public TitledScreen(Text title, Screen previous) {
super(title);
this.previous = previous;
}

@Override
public void render(DrawContext context, int mouseX, int mouseY, float delta) {
super.render(context, mouseX, mouseY, delta);
context.drawCenteredTextWithShadow(this.textRenderer, this.title, this.width / 2, 5, TITLE_COLOR);
}

@Override
public void close() {
this.client.setScreen(this.previous);
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package se.icus.mag.modsettings.gui;
package se.icus.mag.modsettings.gui.widget;

import net.minecraft.client.gui.widget.ButtonWidget;
import net.minecraft.text.Text;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package se.icus.mag.modsettings.gui.widget;

import net.minecraft.client.font.TextRenderer;
import net.minecraft.client.gui.DrawContext;
import net.minecraft.client.gui.widget.ButtonWidget;
import net.minecraft.text.Text;
import net.minecraft.util.Identifier;

public class IconButtonWidget extends ButtonWidget {
private final int textureWidth;
private final int textureHeight;
protected Identifier texture;

public IconButtonWidget(int x, int y, int width, int height,int textureWidth, int textureHeight,
Identifier texture, ButtonWidget.PressAction onPress) {
this(x, y, width, height, textureWidth, textureHeight, onPress);
this.texture = texture;
}

protected IconButtonWidget(int x, int y, int width, int height,int textureWidth, int textureHeight,
ButtonWidget.PressAction onPress) {
super(x, y, width, height, Text.empty(), onPress, DEFAULT_NARRATION_SUPPLIER);
this.textureWidth = textureWidth;
this.textureHeight = textureHeight;
}

@Override
public void renderWidget(DrawContext context, int mouseX, int mouseY, float delta) {
super.renderWidget(context, mouseX, mouseY, delta);
int x = this.getX() + this.getWidth() / 2 - this.textureWidth / 2;
int y = this.getY() + this.getHeight() / 2 - this.textureHeight / 2;
context.drawGuiTexture(this.texture, x, y, this.textureWidth, this.textureHeight);
}

@Override
public void drawMessage(DrawContext context, TextRenderer textRenderer, int color) {
}
}
Loading