From 302728192f0f3176264b7057d07cbffa1299b26a Mon Sep 17 00:00:00 2001 From: Martin Prokoph Date: Mon, 12 May 2025 12:35:20 +0200 Subject: [PATCH] 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. --- .../lib/config/MidnightConfig.java | 36 ++++--- .../fabric/example/MidnightLibExtras.java | 96 +++++++++++++++++++ .../example/config/MidnightConfigExample.java | 16 ++++ .../resources/assets/modid/lang/en_us.json | 1 + 4 files changed, 134 insertions(+), 15 deletions(-) create mode 100644 test-fabric/src/main/java/eu/midnightdust/fabric/example/MidnightLibExtras.java diff --git a/common/src/main/java/eu/midnightdust/lib/config/MidnightConfig.java b/common/src/main/java/eu/midnightdust/lib/config/MidnightConfig.java index 3832f0e..e8e0f56 100755 --- a/common/src/main/java/eu/midnightdust/lib/config/MidnightConfig.java +++ b/common/src/main/java/eu/midnightdust/lib/config/MidnightConfig.java @@ -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> 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 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>) 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 processor = ((BiFunction>) 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); } } diff --git a/test-fabric/src/main/java/eu/midnightdust/fabric/example/MidnightLibExtras.java b/test-fabric/src/main/java/eu/midnightdust/fabric/example/MidnightLibExtras.java new file mode 100644 index 0000000..12deb14 --- /dev/null +++ b/test-fabric/src/main/java/eu/midnightdust/fabric/example/MidnightLibExtras.java @@ -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); + } + } + } +} diff --git a/test-fabric/src/main/java/eu/midnightdust/fabric/example/config/MidnightConfigExample.java b/test-fabric/src/main/java/eu/midnightdust/fabric/example/config/MidnightConfigExample.java index 3ffa54d..80d4a15 100644 --- a/test-fabric/src/main/java/eu/midnightdust/fabric/example/config/MidnightConfigExample.java +++ b/test-fabric/src/main/java/eu/midnightdust/fabric/example/config/MidnightConfigExample.java @@ -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); + } + } + } \ No newline at end of file diff --git a/test-fabric/src/main/resources/assets/modid/lang/en_us.json b/test-fabric/src/main/resources/assets/modid/lang/en_us.json index 702947c..273f0da 100644 --- a/test-fabric/src/main/resources/assets/modid/lang/en_us.json +++ b/test-fabric/src/main/resources/assets/modid/lang/en_us.json @@ -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" } \ No newline at end of file