Skip to content

Commit

Permalink
feat: Add toggle button to show/hide child mods, and a search widget (#…
Browse files Browse the repository at this point in the history
…21)

* chore: Refactor package structure

* refactor: Extract TitledScreen

* refactor: Extract ModSettingsOption to top-level class

* refactor: Rename ModSettingsOption to ModConfigInfo

* feat: Filter list of mod based on search string and if child mods are included

* chore: Fix incorrect whitespace

* refactor: Cleanups and preparing for mod options

* feat: Add icon button widgets

* feat: Add toggle button to show or hide child mods, and a search widget to filter visible mods

* chore: Update changelog
  • Loading branch information
magicus committed Apr 27, 2024
1 parent 15813dc commit 401d036
Show file tree
Hide file tree
Showing 20 changed files with 372 additions and 90 deletions.
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

0 comments on commit 401d036

Please sign in to comment.