feat: new method to manually add widgets

- Also added a keybind widget as an example for this.
It will not be included in the base jar, but can be manually copied into mods that rely on it.
This commit is contained in:
Martin Prokoph
2025-05-12 12:35:20 +02:00
parent 6693580cfa
commit 302728192f
4 changed files with 134 additions and 15 deletions

View File

@@ -14,6 +14,7 @@ import net.minecraft.client.resource.language.I18n;
import net.minecraft.registry.Registries;
import net.minecraft.screen.ScreenTexts;
import net.minecraft.text.Style; import net.minecraft.text.Text;
import net.minecraft.text.TranslatableTextContent;
import net.minecraft.util.Formatting; import net.minecraft.util.Identifier;
import org.jetbrains.annotations.Nullable;
@@ -106,6 +107,10 @@ public abstract class MidnightConfig {
if (index >= list.size()) list.add(value);
else list.set(index, value);
}
public Tooltip getTooltip(boolean isButton) {
String key = this.modid + ".midnightconfig."+this.fieldName+(!isButton ? ".label" : "" )+".tooltip";
return Tooltip.of(isButton && this.error != null ? this.error : I18n.hasTranslation(key) ? Text.translatable(key) : Text.empty());
}
}
public static final Map<String, Class<? extends MidnightConfig>> configClass = new HashMap<>();
@@ -119,11 +124,6 @@ public abstract class MidnightConfig {
public Identifier read(JsonReader in) throws IOException { return Identifier.of(in.nextString()); }
}).setPrettyPrinting().create();
@SuppressWarnings("unused") // Utility for mod authors
public static @Nullable Object getDefaultValue(String modid, String entry) {
String key = modid + ":" + entry;
return entries.containsKey(key) ? entries.get(key).defaultValue : null;
}
public static void loadValuesFromJson(String modid) {
try { gson.fromJson(Files.newBufferedReader(path), configClass.get(modid)); }
catch (Exception e) { write(modid); }
@@ -190,10 +190,6 @@ public abstract class MidnightConfig {
try { return (Class<?>) rawType.getField("TYPE").get(null); // Tries to get primitive types from non-primitives (e.g. Boolean -> boolean)
} catch (NoSuchFieldException | IllegalAccessException ignored) { return rawType; }
}
public static Tooltip getTooltip(EntryInfo info, boolean isButton) {
String key = info.modid + ".midnightconfig."+info.fieldName+(!isButton ? ".label" : "" )+".tooltip";
return Tooltip.of(isButton && info.error != null ? info.error : I18n.hasTranslation(key) ? Text.translatable(key) : Text.empty());
}
private static void textField(EntryInfo info, Function<String,Number> f, Pattern pattern, double min, double max, boolean cast) {
boolean isNumber = pattern != null;
@@ -209,7 +205,7 @@ public abstract class MidnightConfig {
info.error = inLimits? null : Text.literal(value.doubleValue() < min ?
"§cMinimum " + (isNumber? "value" : "length") + (cast? " is " + (int)min : " is " + min) :
"§cMaximum " + (isNumber? "value" : "length") + (cast? " is " + (int)max : " is " + max)).formatted(Formatting.RED);
t.setTooltip(getTooltip(info, true));
t.setTooltip(info.getTooltip(true));
}
info.tempValue = s;
@@ -241,6 +237,15 @@ public abstract class MidnightConfig {
Files.write(path, gson.toJson(getClass(modid)).getBytes());
} catch (Exception e) { e.fillInStackTrace(); }
}
@SuppressWarnings("unused") // Utility for mod authors
public static @Nullable Object getDefaultValue(String modid, String entry) {
String key = modid + ":" + entry;
return entries.containsKey(key) ? entries.get(key).defaultValue : null;
}
public void onTabInit(String tabName, MidnightConfigListWidget list, MidnightConfigScreen screen) {}
@Environment(EnvType.CLIENT)
public static Screen getScreen(Screen parent, String modid) {
return new MidnightConfigScreen(parent, modid);
@@ -295,9 +300,9 @@ public abstract class MidnightConfig {
public void updateButtons() {
if (this.list != null) {
for (ButtonEntry entry : this.list.children()) {
if (entry.buttons != null && entry.buttons.size() > 1) {
if (entry.buttons != null && entry.buttons.size() > 1 && entry.info.field != null) {
if (entry.buttons.get(0) instanceof ClickableWidget widget)
if (widget.isFocused() || widget.isHovered()) widget.setTooltip(getTooltip(entry.info, true));
if (widget.isFocused() || widget.isHovered()) widget.setTooltip(entry.info.getTooltip(true));
if (entry.buttons.get(1) instanceof ButtonWidget button)
button.active = !Objects.equals(String.valueOf(entry.info.value), String.valueOf(entry.info.defaultValue)) && entry.info.conditionsMet;
}}}}
@@ -337,6 +342,7 @@ public abstract class MidnightConfig {
this.list.clear(); fillList();
}
public void fillList() {
MidnightConfig.getClass(modid).onTabInit(prevTab.getTitle().getContent() instanceof TranslatableTextContent translatable ? translatable.getKey().replace("%s.midnightconfig.category.".formatted(modid), "") : prevTab.getTitle().toString(), list, this);
for (EntryInfo info : entries.values()) {
if (!info.conditionsMet) {
boolean visibleButLocked = false;
@@ -361,7 +367,7 @@ public abstract class MidnightConfig {
var values = (Map.Entry<ButtonWidget.PressAction, Function<Object, Text>>) info.function;
if (info.dataType.isEnum())
values.setValue(value -> Text.translatable(translationPrefix + "enum." + info.dataType.getSimpleName() + "." + info.value.toString()));
widget = ButtonWidget.builder(values.getValue().apply(info.value), values.getKey()).dimensions(width - 185, 0, 150, 20).tooltip(getTooltip(info, true)).build();
widget = ButtonWidget.builder(values.getValue().apply(info.value), values.getKey()).dimensions(width - 185, 0, 150, 20).tooltip(info.getTooltip(true)).build();
if (info.dataType == boolean.class) info.actionButton = CheckboxWidget.builder(Text.empty(), textRenderer).callback((checkbox, checked) -> values.getKey().onPress((ButtonWidget) widget)).checked((Boolean) info.value).pos(widget.getX(), 1).build();
} else if (e.isSlider())
widget = new MidnightSliderWidget(width - 185, 0, 150, 20, Text.of(info.tempValue), (Double.parseDouble(info.tempValue) - e.min()) / (e.max() - e.min()), info);
@@ -371,7 +377,7 @@ public abstract class MidnightConfig {
Predicate<String> processor = ((BiFunction<TextFieldWidget, ButtonWidget, Predicate<String>>) info.function).apply(textField, done);
textField.setTextPredicate(processor);
}
widget.setTooltip(getTooltip(info, true));
widget.setTooltip(info.getTooltip(true));
ButtonWidget cycleButton = null;
if (info.field.getType() == List.class) {
@@ -470,7 +476,7 @@ public abstract class MidnightConfig {
if (text != null && (!text.getString().contains("spacer") || !buttons.isEmpty())) {
title = new MultilineTextWidget((centered) ? (scaledWidth / 2 - (textRenderer.getWidth(text) / 2)) : 12, 0, Text.of(text), textRenderer);
if (info != null) title.setTooltip(getTooltip(info, false));
if (info != null) title.setTooltip(info.getTooltip(false));
title.setMaxWidth(buttons.size() > 1 ? buttons.get(1).getX() - 24 : scaledWidth - 24);
}
}

View File

@@ -0,0 +1,96 @@
package eu.midnightdust.fabric.example;
import com.google.common.collect.Lists;
import eu.midnightdust.lib.config.MidnightConfig;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.gui.tooltip.Tooltip;
import net.minecraft.client.gui.widget.ButtonWidget;
import net.minecraft.client.gui.widget.ClickableWidget;
import net.minecraft.client.gui.widget.TextIconButtonWidget;
import net.minecraft.client.option.KeyBinding;
import net.minecraft.client.util.InputUtil;
import net.minecraft.text.MutableText;
import net.minecraft.text.Text;
import net.minecraft.util.Formatting;
import net.minecraft.util.Identifier;
import org.jetbrains.annotations.Nullable;
import org.lwjgl.glfw.GLFW;
/*
Pre-made additional (niche) functionality that is not included in MidnightLib to keep the file size small.
Feel free to copy the parts you need :)
*/
public class MidnightLibExtras {
public static class KeybindButton extends ButtonWidget {
public static ButtonWidget focusedButton;
public static void add(KeyBinding binding, MidnightConfig.MidnightConfigListWidget list, MidnightConfig.MidnightConfigScreen screen) {
KeybindButton editButton = new KeybindButton(screen.width - 185, 0, 150, 20, binding);
TextIconButtonWidget resetButton = TextIconButtonWidget.builder(Text.translatable("controls.reset"), (button -> {
binding.setBoundKey(binding.getDefaultKey());
screen.updateList();
}), true).texture(Identifier.of("midnightlib","icon/reset"), 12, 12).dimension(20, 20).build();
resetButton.setPosition(screen.width - 205 + 150 + 25, 0);
editButton.resetButton = resetButton;
editButton.updateMessage(false);
MidnightConfig.EntryInfo info = new MidnightConfig.EntryInfo(null, screen.modid);
list.addButton(Lists.newArrayList(editButton, resetButton), Text.translatable(binding.getTranslationKey()), info);
}
private final KeyBinding binding;
private @Nullable ClickableWidget resetButton;
public KeybindButton(int x, int y, int width, int height, KeyBinding binding) {
super(x, y, width, height, binding.getBoundKeyLocalizedText(), (button) -> {
((KeybindButton) button).updateMessage(true);
focusedButton = button;
}, (textSupplier) -> binding.isUnbound() ? Text.translatable("narrator.controls.unbound", binding.getTranslationKey()) : Text.translatable("narrator.controls.bound", binding.getTranslationKey(), textSupplier.get()));
this.binding = binding;
updateMessage(false);
}
@Override
public boolean keyPressed(int keyCode, int scanCode, int modifiers) {
if (focusedButton == this) {
if (keyCode == GLFW.GLFW_KEY_ESCAPE) {
this.binding.setBoundKey(InputUtil.UNKNOWN_KEY);
} else {
this.binding.setBoundKey(InputUtil.fromKeyCode(keyCode, scanCode));
}
updateMessage(false);
focusedButton = null;
return true;
}
return super.keyPressed(keyCode, scanCode, modifiers);
}
public void updateMessage(boolean focused) {
boolean hasConflicts = false;
MutableText conflictingBindings = Text.empty();
if (focused) this.setMessage(Text.literal("> ").append(this.binding.getBoundKeyLocalizedText().copy().formatted(Formatting.WHITE, Formatting.UNDERLINE)).append(" <").formatted(Formatting.YELLOW));
else {
this.setMessage(this.binding.getBoundKeyLocalizedText());
if (!this.binding.isUnbound()) {
for(KeyBinding keyBinding : MinecraftClient.getInstance().options.allKeys) {
if (keyBinding != this.binding && this.binding.equals(keyBinding)) {
if (hasConflicts) conflictingBindings.append(", ");
hasConflicts = true;
conflictingBindings.append(Text.translatable(keyBinding.getTranslationKey()));
}
}
}
}
if (this.resetButton != null) this.resetButton.active = !this.binding.isDefault();
if (hasConflicts) {
this.setMessage(Text.literal("[ ").append(this.getMessage().copy().formatted(Formatting.WHITE)).append(" ]").formatted(Formatting.RED));
this.setTooltip(Tooltip.of(Text.translatable("controls.keybinds.duplicateKeybinds", conflictingBindings)));
} else {
this.setTooltip(null);
}
}
}
}

View File

@@ -1,12 +1,15 @@
package eu.midnightdust.fabric.example.config;
import com.google.common.collect.Lists;
import eu.midnightdust.fabric.example.MidnightLibExtras;
import eu.midnightdust.lib.config.MidnightConfig;
import net.minecraft.client.MinecraftClient;
import net.minecraft.util.Identifier;
import javax.swing.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
/** Every option in a MidnightConfig class has to be public and static, so we can access it from other classes.
* The config class also has to extend MidnightConfig*/
@@ -18,6 +21,7 @@ public class MidnightConfigExample extends MidnightConfig {
public static final String LISTS = "lists";
public static final String FILES = "files";
public static final String CONDITIONS = "conditions";
public static final String EXTRAS = "extras";
@Comment(category = TEXT) public static Comment text1; // Comments are rendered like an option without a button and are excluded from the config file
@Comment(category = TEXT, centered = true) public static Comment text2; // Centered comments are the same as normal ones - just centered!
@@ -115,4 +119,16 @@ public class MidnightConfigExample extends MidnightConfig {
@Comment(category = CONDITIONS, name="You disabled MidnightLib's config screen list. Why? :(", centered = true) public static Comment why;
public static int imposter = 16777215; // - Entries without an @Entry or @Comment annotation are ignored
@Condition(requiredModId = "thismoddoesnotexist")
@Comment(category = EXTRAS) public static Comment iAmJustADummy; // We only have this to initialize an empty tab for the keybinds below
@Override
public void onTabInit(String tabName, MidnightConfigListWidget list, MidnightConfigScreen screen) {
if (Objects.equals(tabName, EXTRAS)) {
MidnightLibExtras.KeybindButton.add(MinecraftClient.getInstance().options.advancementsKey, list, screen);
MidnightLibExtras.KeybindButton.add(MinecraftClient.getInstance().options.dropKey, list, screen);
}
}
}

View File

@@ -24,5 +24,6 @@
"modid.midnightconfig.category.lists": "Lists",
"modid.midnightconfig.category.files": "Files",
"modid.midnightconfig.category.conditions": "Quiz",
"modid.midnightconfig.category.extras": "Extras",
"modid.midnightconfig.category.multiConditions": "Multi-Conditions"
}