aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCrendgrim <Crendgrim@users.noreply.github.com>2023-08-31 00:06:47 +0200
committerGitHub <noreply@github.com>2023-08-30 23:06:47 +0100
commit554646dbd857e2fab1be8339ce8d0231ef2dbb4c (patch)
treeb55e6f1d282c18e743857bba3c9d5e4f226200ed
parentc11566072608683034864dbd4e0d3f5afa067537 (diff)
downloadYetAnotherConfigLib-554646dbd857e2fab1be8339ce8d0231ef2dbb4c.tar.gz
YetAnotherConfigLib-554646dbd857e2fab1be8339ce8d0231ef2dbb4c.tar.bz2
YetAnotherConfigLib-554646dbd857e2fab1be8339ce8d0231ef2dbb4c.zip
Add dropdown controllers (#95)
-rw-r--r--common/src/main/java/dev/isxander/yacl3/api/controller/DropdownStringControllerBuilder.java18
-rw-r--r--common/src/main/java/dev/isxander/yacl3/api/controller/ItemControllerBuilder.java11
-rw-r--r--common/src/main/java/dev/isxander/yacl3/config/GsonConfigInstance.java18
-rw-r--r--common/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/Dropdown.java43
-rw-r--r--common/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/ItemField.java17
-rw-r--r--common/src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/DropdownImpl.java19
-rw-r--r--common/src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/ItemFieldImpl.java17
-rw-r--r--common/src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/OptionFactoryRegistry.java2
-rw-r--r--common/src/main/java/dev/isxander/yacl3/config/v2/impl/serializer/GsonConfigSerializer.java17
-rw-r--r--common/src/main/java/dev/isxander/yacl3/gui/controllers/dropdown/AbstractDropdownController.java80
-rw-r--r--common/src/main/java/dev/isxander/yacl3/gui/controllers/dropdown/AbstractDropdownControllerElement.java238
-rw-r--r--common/src/main/java/dev/isxander/yacl3/gui/controllers/dropdown/DropdownStringController.java34
-rw-r--r--common/src/main/java/dev/isxander/yacl3/gui/controllers/dropdown/DropdownStringControllerElement.java31
-rw-r--r--common/src/main/java/dev/isxander/yacl3/gui/controllers/dropdown/ItemController.java68
-rw-r--r--common/src/main/java/dev/isxander/yacl3/gui/controllers/dropdown/ItemControllerElement.java87
-rw-r--r--common/src/main/java/dev/isxander/yacl3/gui/utils/ItemRegistryHelper.java104
-rw-r--r--common/src/main/java/dev/isxander/yacl3/impl/controller/DropdownStringControllerBuilderImpl.java49
-rw-r--r--common/src/main/java/dev/isxander/yacl3/impl/controller/ItemControllerBuilderImpl.java18
-rw-r--r--common/src/main/resources/assets/yet_another_config_lib/lang/en_us.json2
-rw-r--r--test-common/src/main/java/dev/isxander/yacl3/test/AutogenConfigTest.java12
-rw-r--r--test-common/src/main/java/dev/isxander/yacl3/test/ConfigTest.java8
-rw-r--r--test-common/src/main/java/dev/isxander/yacl3/test/GuiTest.java41
22 files changed, 928 insertions, 6 deletions
diff --git a/common/src/main/java/dev/isxander/yacl3/api/controller/DropdownStringControllerBuilder.java b/common/src/main/java/dev/isxander/yacl3/api/controller/DropdownStringControllerBuilder.java
new file mode 100644
index 0000000..3f5fb33
--- /dev/null
+++ b/common/src/main/java/dev/isxander/yacl3/api/controller/DropdownStringControllerBuilder.java
@@ -0,0 +1,18 @@
+package dev.isxander.yacl3.api.controller;
+
+import dev.isxander.yacl3.api.Option;
+import dev.isxander.yacl3.impl.controller.DropdownStringControllerBuilderImpl;
+
+import java.util.List;
+
+public interface DropdownStringControllerBuilder extends StringControllerBuilder {
+ DropdownStringControllerBuilder values(List<String> values);
+ DropdownStringControllerBuilder values(String... values);
+ DropdownStringControllerBuilder allowEmptyValue(boolean allowEmptyValue);
+ DropdownStringControllerBuilder allowAnyValue(boolean allowAnyValue);
+
+
+ static DropdownStringControllerBuilder create(Option<String> option) {
+ return new DropdownStringControllerBuilderImpl(option);
+ }
+}
diff --git a/common/src/main/java/dev/isxander/yacl3/api/controller/ItemControllerBuilder.java b/common/src/main/java/dev/isxander/yacl3/api/controller/ItemControllerBuilder.java
new file mode 100644
index 0000000..5a1f5fa
--- /dev/null
+++ b/common/src/main/java/dev/isxander/yacl3/api/controller/ItemControllerBuilder.java
@@ -0,0 +1,11 @@
+package dev.isxander.yacl3.api.controller;
+
+import dev.isxander.yacl3.api.Option;
+import dev.isxander.yacl3.impl.controller.ItemControllerBuilderImpl;
+import net.minecraft.world.item.Item;
+
+public interface ItemControllerBuilder extends ControllerBuilder<Item> {
+ static ItemControllerBuilder create(Option<Item> option) {
+ return new ItemControllerBuilderImpl(option);
+ }
+}
diff --git a/common/src/main/java/dev/isxander/yacl3/config/GsonConfigInstance.java b/common/src/main/java/dev/isxander/yacl3/config/GsonConfigInstance.java
index acbf338..deff6d7 100644
--- a/common/src/main/java/dev/isxander/yacl3/config/GsonConfigInstance.java
+++ b/common/src/main/java/dev/isxander/yacl3/config/GsonConfigInstance.java
@@ -1,9 +1,12 @@
package dev.isxander.yacl3.config;
import com.google.gson.*;
+import dev.isxander.yacl3.gui.utils.ItemRegistryHelper;
import dev.isxander.yacl3.impl.utils.YACLConstants;
+import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.network.chat.Component;
import net.minecraft.network.chat.Style;
+import net.minecraft.world.item.Item;
import java.awt.*;
import java.io.IOException;
@@ -66,6 +69,7 @@ public class GsonConfigInstance<T> extends ConfigInstance<T> {
.registerTypeHierarchyAdapter(Component.class, new Component.Serializer())
.registerTypeHierarchyAdapter(Style.class, new Style.Serializer())
.registerTypeHierarchyAdapter(Color.class, new ColorTypeAdapter())
+ .registerTypeHierarchyAdapter(Item.class, new ItemTypeAdapter())
.serializeNulls()
.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
.create();
@@ -129,6 +133,17 @@ public class GsonConfigInstance<T> extends ConfigInstance<T> {
return new JsonPrimitive(color.getRGB());
}
}
+ public static class ItemTypeAdapter implements JsonSerializer<Item>, JsonDeserializer<Item> {
+ @Override
+ public Item deserialize(JsonElement jsonElement, Type type, JsonDeserializationContext jsonDeserializationContext) throws JsonParseException {
+ return ItemRegistryHelper.getItemFromName(jsonElement.getAsString());
+ }
+
+ @Override
+ public JsonElement serialize(Item item, Type type, JsonSerializationContext jsonSerializationContext) {
+ return new JsonPrimitive(BuiltInRegistries.ITEM.getKey(item).toString());
+ }
+ }
/**
* Creates a builder for a GSON config instance.
@@ -148,7 +163,8 @@ public class GsonConfigInstance<T> extends ConfigInstance<T> {
.serializeNulls()
.registerTypeHierarchyAdapter(Component.class, new Component.Serializer())
.registerTypeHierarchyAdapter(Style.class, new Style.Serializer())
- .registerTypeHierarchyAdapter(Color.class, new ColorTypeAdapter());
+ .registerTypeHierarchyAdapter(Color.class, new ColorTypeAdapter())
+ .registerTypeHierarchyAdapter(Item.class, new ItemTypeAdapter());
private Builder(Class<T> configClass) {
this.configClass = configClass;
diff --git a/common/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/Dropdown.java b/common/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/Dropdown.java
new file mode 100644
index 0000000..44239d5
--- /dev/null
+++ b/common/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/Dropdown.java
@@ -0,0 +1,43 @@
+package dev.isxander.yacl3.config.v2.api.autogen;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * An option factory.
+ * <p>
+ * This creates a regular option with a
+ * {@link dev.isxander.yacl3.api.controller.DropdownStringControllerBuilder} controller.
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.FIELD)
+public @interface Dropdown {
+ /**
+ * The allowed values for the field. These will be shown in a dropdown
+ * that the user can filter and select from.
+ * <p>
+ * Only values in this list will be accepted and written to the config
+ * file, unless {@link #allow()} is set to ${@code ALLOW_ANY}.
+ * <p>
+ * Empty string is a valid value only if it appears in this list, or if
+ * {@link #allow()} is set to {@code ALLOW_EMPTY} or {@code ALLOW_ANY}.
+ */
+ String[] values();
+
+ /**
+ * Whether to accept the empty string as a valid value if it does not
+ * already appear in {@link #values()}. If it already appears there,
+ * the value of this does not apply.
+ */
+ boolean allowEmptyValue() default false;
+
+ /**
+ * Whether to accept any string as a valid value. The list of strings
+ * supplied in {@link #values()} are only used as dropdown suggestions.
+ * Empty strings are still prohibited unless the empty string appears in
+ * {@link #values()} or {@link #allowEmptyValue()}.
+ */
+ boolean allowAnyValue() default false;
+}
diff --git a/common/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/ItemField.java b/common/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/ItemField.java
new file mode 100644
index 0000000..84d2c7a
--- /dev/null
+++ b/common/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/ItemField.java
@@ -0,0 +1,17 @@
+package dev.isxander.yacl3.config.v2.api.autogen;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * An option factory.
+ * <p>
+ * This creates a regular option with a
+ * {@link dev.isxander.yacl3.api.controller.ItemControllerBuilder} controller.
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.FIELD)
+public @interface ItemField {
+}
diff --git a/common/src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/DropdownImpl.java b/common/src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/DropdownImpl.java
new file mode 100644
index 0000000..c487aab
--- /dev/null
+++ b/common/src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/DropdownImpl.java
@@ -0,0 +1,19 @@
+package dev.isxander.yacl3.config.v2.impl.autogen;
+
+import dev.isxander.yacl3.api.Option;
+import dev.isxander.yacl3.api.controller.ControllerBuilder;
+import dev.isxander.yacl3.api.controller.DropdownStringControllerBuilder;
+import dev.isxander.yacl3.config.v2.api.ConfigField;
+import dev.isxander.yacl3.config.v2.api.autogen.Dropdown;
+import dev.isxander.yacl3.config.v2.api.autogen.OptionAccess;
+import dev.isxander.yacl3.config.v2.api.autogen.SimpleOptionFactory;
+
+public class DropdownImpl extends SimpleOptionFactory<Dropdown, String> {
+ @Override
+ protected ControllerBuilder<String> createController(Dropdown annotation, ConfigField<String> field, OptionAccess storage, Option<String> option) {
+ return DropdownStringControllerBuilder.create(option)
+ .values(annotation.values())
+ .allowEmptyValue(annotation.allowEmptyValue())
+ .allowAnyValue(annotation.allowAnyValue());
+ }
+}
diff --git a/common/src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/ItemFieldImpl.java b/common/src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/ItemFieldImpl.java
new file mode 100644
index 0000000..2802f5c
--- /dev/null
+++ b/common/src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/ItemFieldImpl.java
@@ -0,0 +1,17 @@
+package dev.isxander.yacl3.config.v2.impl.autogen;
+
+import dev.isxander.yacl3.api.Option;
+import dev.isxander.yacl3.api.controller.ControllerBuilder;
+import dev.isxander.yacl3.api.controller.ItemControllerBuilder;
+import dev.isxander.yacl3.config.v2.api.ConfigField;
+import dev.isxander.yacl3.config.v2.api.autogen.ItemField;
+import dev.isxander.yacl3.config.v2.api.autogen.OptionAccess;
+import dev.isxander.yacl3.config.v2.api.autogen.SimpleOptionFactory;
+import net.minecraft.world.item.Item;
+
+public class ItemFieldImpl extends SimpleOptionFactory<ItemField, Item> {
+ @Override
+ protected ControllerBuilder<Item> createController(ItemField annotation, ConfigField<Item> field, OptionAccess storage, Option<Item> option) {
+ return ItemControllerBuilder.create(option);
+ }
+}
diff --git a/common/src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/OptionFactoryRegistry.java b/common/src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/OptionFactoryRegistry.java
index 4bcf5d6..4f6e3c7 100644
--- a/common/src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/OptionFactoryRegistry.java
+++ b/common/src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/OptionFactoryRegistry.java
@@ -31,6 +31,8 @@ public class OptionFactoryRegistry {
registerOptionFactory(EnumCycler.class, new EnumCyclerImpl());
registerOptionFactory(StringField.class, new StringFieldImpl());
registerOptionFactory(ColorField.class, new ColorFieldImpl());
+ registerOptionFactory(Dropdown.class, new DropdownImpl());
+ registerOptionFactory(ItemField.class, new ItemFieldImpl());
registerOptionFactory(Label.class, new LabelImpl());
registerOptionFactory(ListGroup.class, new ListGroupImpl<>());
diff --git a/common/src/main/java/dev/isxander/yacl3/config/v2/impl/serializer/GsonConfigSerializer.java b/common/src/main/java/dev/isxander/yacl3/config/v2/impl/serializer/GsonConfigSerializer.java
index 1df9cfb..d308c23 100644
--- a/common/src/main/java/dev/isxander/yacl3/config/v2/impl/serializer/GsonConfigSerializer.java
+++ b/common/src/main/java/dev/isxander/yacl3/config/v2/impl/serializer/GsonConfigSerializer.java
@@ -1,12 +1,16 @@
package dev.isxander.yacl3.config.v2.impl.serializer;
import com.google.gson.*;
+import dev.isxander.yacl3.config.GsonConfigInstance;
import dev.isxander.yacl3.config.v2.api.*;
import dev.isxander.yacl3.config.v2.api.serializer.GsonConfigSerializerBuilder;
+import dev.isxander.yacl3.gui.utils.ItemRegistryHelper;
import dev.isxander.yacl3.impl.utils.YACLConstants;
import dev.isxander.yacl3.platform.YACLPlatform;
+import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.network.chat.Component;
import net.minecraft.network.chat.Style;
+import net.minecraft.world.item.Item;
import org.quiltmc.parsers.json.JsonReader;
import org.quiltmc.parsers.json.JsonWriter;
import org.quiltmc.parsers.json.gson.GsonReader;
@@ -129,6 +133,18 @@ public class GsonConfigSerializer<T> extends ConfigSerializer<T> {
}
}
+ public static class ItemTypeAdapter implements JsonSerializer<Item>, JsonDeserializer<Item> {
+ @Override
+ public Item deserialize(JsonElement jsonElement, Type type, JsonDeserializationContext jsonDeserializationContext) throws JsonParseException {
+ return ItemRegistryHelper.getItemFromName(jsonElement.getAsString());
+ }
+
+ @Override
+ public JsonElement serialize(Item item, Type type, JsonSerializationContext jsonSerializationContext) {
+ return new JsonPrimitive(BuiltInRegistries.ITEM.getKey(item).toString());
+ }
+ }
+
public static class Builder<T> implements GsonConfigSerializerBuilder<T> {
private final ConfigClassHandler<T> config;
private Path path;
@@ -139,6 +155,7 @@ public class GsonConfigSerializer<T> extends ConfigSerializer<T> {
.registerTypeHierarchyAdapter(Component.class, new Component.Serializer())
.registerTypeHierarchyAdapter(Style.class, new Style.Serializer())
.registerTypeHierarchyAdapter(Color.class, new ColorTypeAdapter())
+ .registerTypeHierarchyAdapter(Item.class, new ItemTypeAdapter())
.setPrettyPrinting();
public Builder(ConfigClassHandler<T> config) {
diff --git a/common/src/main/java/dev/isxander/yacl3/gui/controllers/dropdown/AbstractDropdownController.java b/common/src/main/java/dev/isxander/yacl3/gui/controllers/dropdown/AbstractDropdownController.java
new file mode 100644
index 0000000..8251f9e
--- /dev/null
+++ b/common/src/main/java/dev/isxander/yacl3/gui/controllers/dropdown/AbstractDropdownController.java
@@ -0,0 +1,80 @@
+package dev.isxander.yacl3.gui.controllers.dropdown;
+
+import dev.isxander.yacl3.api.Option;
+import dev.isxander.yacl3.gui.controllers.string.IStringController;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+public abstract class AbstractDropdownController<T> implements IStringController<T> {
+ protected final Option<T> option;
+ private final List<String> allowedValues;
+ public final boolean allowEmptyValue;
+ public final boolean allowAnyValue;
+
+ /**
+ * Constructs a dropdown controller
+ *
+ * @param option bound option
+ * @param allowedValues possible values
+ */
+ protected AbstractDropdownController(Option<T> option, List<String> allowedValues, boolean allowEmptyValue, boolean allowAnyValue) {
+ this.option = option;
+ this.allowedValues = allowedValues;
+ this.allowEmptyValue = allowEmptyValue;
+ this.allowAnyValue = allowAnyValue;
+ }
+
+ protected AbstractDropdownController(Option<T> option) {
+ this(option, Collections.emptyList(), false, false);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Option<T> option() {
+ return option;
+ }
+
+ public List<String> getAllowedValues() {
+ return getAllowedValues("");
+ }
+ public List<String> getAllowedValues(String inputField) {
+ List<String> values = new ArrayList<>(allowedValues);
+ if (allowEmptyValue && !values.contains("")) values.add("");
+ if (allowAnyValue && !inputField.isBlank() && !allowedValues.contains(inputField)) {
+ values.add(inputField);
+ }
+ String currentValue = getString();
+ if (allowAnyValue && !allowedValues.contains(currentValue)) {
+ values.add(currentValue);
+ }
+ return values;
+ }
+
+ public boolean isValueValid(String value) {
+ if (value.isBlank()) return allowEmptyValue;
+ return allowAnyValue || getAllowedValues().contains(value);
+ }
+
+ protected String getValidValue(String value) {
+ return getValidValue(value, 0);
+ }
+ protected String getValidValue(String value, int offset) {
+ if (offset == -1) return getString();
+
+ return getAllowedValues(value).stream()
+ .filter(val -> val.toLowerCase().contains(value.toLowerCase()))
+ .sorted((s1, s2) -> {
+ if (s1.startsWith(value) && !s2.startsWith(value)) return -1;
+ if (!s1.startsWith(value) && s2.startsWith(value)) return 1;
+ return s1.compareTo(s2);
+ })
+ .skip(offset)
+ .findFirst()
+ .orElseGet(this::getString);
+ }
+
+}
diff --git a/common/src/main/java/dev/isxander/yacl3/gui/controllers/dropdown/AbstractDropdownControllerElement.java b/common/src/main/java/dev/isxander/yacl3/gui/controllers/dropdown/AbstractDropdownControllerElement.java
new file mode 100644
index 0000000..f91fc41
--- /dev/null
+++ b/common/src/main/java/dev/isxander/yacl3/gui/controllers/dropdown/AbstractDropdownControllerElement.java
@@ -0,0 +1,238 @@
+package dev.isxander.yacl3.gui.controllers.dropdown;
+
+import com.mojang.blaze3d.platform.InputConstants;
+import com.mojang.blaze3d.vertex.PoseStack;
+import dev.isxander.yacl3.api.utils.Dimension;
+import dev.isxander.yacl3.gui.YACLScreen;
+import dev.isxander.yacl3.gui.controllers.string.StringControllerElement;
+import dev.isxander.yacl3.gui.utils.GuiUtils;
+import net.minecraft.ChatFormatting;
+import net.minecraft.client.gui.GuiGraphics;
+import net.minecraft.client.gui.screens.Screen;
+import net.minecraft.network.chat.Component;
+
+import java.awt.Color;
+import java.util.List;
+import java.util.function.Consumer;
+
+public abstract class AbstractDropdownControllerElement<T, U> extends StringControllerElement {
+ public static final int MAX_SHOWN_NUMBER_OF_ITEMS = 7;
+
+ private final AbstractDropdownController<T> dropdownController;
+ protected boolean dropdownVisible = false;
+ // Stores the current selection position. The item at this position in the dropdown list will be chosen as the
+ // accepted value when the element is closed.
+ protected int selectedIndex = 0;
+ // Stores a cached list of matching values
+ protected List<U> matchingValues = null;
+
+ public AbstractDropdownControllerElement(AbstractDropdownController<T> control, YACLScreen screen, Dimension<Integer> dim) {
+ super(control, screen, dim, false);
+ this.dropdownController = control;
+ this.dropdownController.option.addListener((opt, val) -> this.matchingValues = this.computeMatchingValues());
+ }
+
+ public void showDropdown() {
+ dropdownVisible = true;
+ selectedIndex = 0;
+ }
+
+ public void closeDropdown() {
+ dropdownVisible = false;
+ ensureValidValue();
+ }
+
+ public void ensureValidValue() {
+ inputField = dropdownController.getValidValue(inputField, selectedIndex);
+ this.matchingValues = this.computeMatchingValues();
+ }
+
+ @Override
+ public boolean mouseClicked(double mouseX, double mouseY, int button) {
+ if (super.mouseClicked(mouseX, mouseY, button)) {
+ if (!dropdownVisible) {
+ showDropdown();
+ doSelectAll();
+ }
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public void setFocused(boolean focused) {
+ if (focused) {
+ doSelectAll();
+ super.setFocused(true);
+ } else unfocus();
+ }
+
+ @Override
+ public void unfocus() {
+ closeDropdown();
+ super.unfocus();
+ }
+
+ @Override
+ public boolean keyPressed(int keyCode, int scanCode, int modifiers) {
+ if (!inputFieldFocused)
+ return false;
+ if (dropdownVisible) {
+ switch (keyCode) {
+ case InputConstants.KEY_DOWN -> {
+ selectNextEntry();
+ return true;
+ }
+ case InputConstants.KEY_UP -> {
+ selectPreviousEntry();
+ return true;
+ }
+ case InputConstants.KEY_TAB -> {
+ if (Screen.hasShiftDown()) {
+ selectPreviousEntry();
+ } else {
+ selectNextEntry();
+ }
+ return true;
+ }
+ }
+ } else {
+ if (keyCode == InputConstants.KEY_RETURN || keyCode == InputConstants.KEY_NUMPADENTER) {
+ showDropdown();
+ return true;
+ }
+ }
+ return super.keyPressed(keyCode, scanCode, modifiers);
+ }
+
+ @Override
+ public boolean charTyped(char chr, int modifiers) {
+ if (!dropdownVisible) {
+ showDropdown();
+ }
+ return super.charTyped(chr, modifiers);
+ }
+
+ @Override
+ protected int getValueColor() {
+ if (inputFieldFocused) {
+ if (!dropdownController.isValueValid(inputField)) {
+ return 0xFFF06080;
+ }
+ }
+ return super.getValueColor();
+ }
+
+ public void selectNextEntry() {
+ if (selectedIndex == getDropdownLength() - 1) {
+ selectedIndex = 0;
+ } else {
+ selectedIndex++;
+ }
+ }
+
+ public void selectPreviousEntry() {
+ if (selectedIndex == 0) {
+ selectedIndex = getDropdownLength() - 1;
+ } else {
+ selectedIndex--;
+ }
+ }
+
+ public int getDropdownLength() {
+ return matchingValues.size();
+ }
+
+ @Override
+ public boolean modifyInput(Consumer<StringBuilder> builder) {
+ boolean success = super.modifyInput(builder);
+ if (success) {
+ this.matchingValues = this.computeMatchingValues();
+ }
+ return success;
+ }
+
+ public abstract List<U> computeMatchingValues();
+
+ public boolean matchingValue(String value) {
+ return value.toLowerCase().contains(inputField.toLowerCase());
+ }
+
+ @Override
+ public void render(GuiGraphics graphics, int mouseX, int mouseY, float delta) {
+ if (matchingValues == null) matchingValues = computeMatchingValues();
+
+ super.render(graphics, mouseX, mouseY, delta);
+
+ if (inputFieldFocused && dropdownVisible) {
+ PoseStack matrices = graphics.pose();
+ matrices.pushPose();
+ matrices.translate(0, 0, 200);
+ renderDropdown(graphics);
+ matrices.popPose();
+ }
+ }
+
+ public void renderDropdown(GuiGraphics graphics) {
+ if (matchingValues.size() == 0) return;
+ // Limit the visible options to allow scrolling through the suggestion list
+ int begin = Math.max(0, selectedIndex - MAX_SHOWN_NUMBER_OF_ITEMS / 2);
+ int end = begin + MAX_SHOWN_NUMBER_OF_ITEMS;
+ if (end >= matchingValues.size()) {
+ end = matchingValues.size();
+ begin = Math.max(0, end - MAX_SHOWN_NUMBER_OF_ITEMS);
+ }
+
+ renderDropdownBackground(graphics, end - begin);
+ if (matchingValues.size() >= 1) {
+ // Highlight the currently selected element
+ graphics.setColor(0.0f, 0.0f, 0.0f, 0.5f);
+ int x = getDimension().x();
+ int y = getDimension().yLimit() + 2 + getDimension().height() * (selectedIndex - begin);
+ graphics.fill(x, y, x + getDimension().width(), y + getDimension().height(), -1);
+ graphics.setColor(1.0f, 1.0f, 1.0f, 1.0f);
+ graphics.renderOutline(x, y, getDimension().width(), getDimension().height(), -1);
+
+ }
+
+ int n = 1;
+ for (int i = begin; i < end; ++i) {
+ renderDropdownEntry(graphics, matchingValues.get(i), n);
+ ++n;
+ }
+ }
+
+ protected int getDropdownEntryPadding() {
+ return 0;
+ }
+
+ protected void renderDropdownEntry(GuiGraphics graphics, U value, int n) {
+ String entry = getString(value);
+ int color = -1;
+ Component text;
+ if (entry.isBlank()) {
+ text = Component.translatable("yacl.control.text.blank").withStyle(ChatFormatting.GRAY);
+ } else {
+ text = shortenString(entry);
+ }
+ graphics.drawString(textRenderer, text, getDimension().xLimit() - textRenderer.width(text) - getDecorationPadding() - getDropdownEntryPadding(), getTextY() + n * getDimension().height() + 2, color, true);
+ }
+
+ public abstract String getString(U object);
+
+ public Component shortenString(String value) {
+ return Component.literal(GuiUtils.shortenString(value, textRenderer, getDimension().width() - 20, "..."));
+ }
+
+ public void renderDropdownBackground(GuiGraphics graphics, int numberOfItems) {
+ graphics.setColor(0.25f, 0.25f, 0.25f, 1.0f);
+ graphics.blit(Screen.BACKGROUND_LOCATION, getDimension().x(), getDimension().yLimit() + 2, 0, 0.0f, 0.0f, getDimension().width(), getDimension().height() * numberOfItems + 2, 32, 32);
+ graphics.setColor(1.0f, 1.0f, 1.0f, 1.0f);
+ graphics.renderOutline(getDimension().x(), getDimension().yLimit() + 2, getDimension().width(), getDimension().height() * numberOfItems, -1);
+ }
+
+ protected int getDecorationPadding() {
+ return super.getXPadding();
+ }
+
+}
diff --git a/common/src/main/java/dev/isxander/yacl3/gui/controllers/dropdown/DropdownStringController.java b/common/src/main/java/dev/isxander/yacl3/gui/controllers/dropdown/DropdownStringController.java
new file mode 100644
index 0000000..fafc759
--- /dev/null
+++ b/common/src/main/java/dev/isxander/yacl3/gui/controllers/dropdown/DropdownStringController.java
@@ -0,0 +1,34 @@
+package dev.isxander.yacl3.gui.controllers.dropdown;
+
+import dev.isxander.yacl3.api.Option;
+import dev.isxander.yacl3.api.utils.Dimension;
+import dev.isxander.yacl3.gui.AbstractWidget;
+import dev.isxander.yacl3.gui.YACLScreen;
+
+import java.util.List;
+
+public class DropdownStringController extends AbstractDropdownController<String> {
+
+ public DropdownStringController(Option<String> option, List<String> allowedValues, boolean allowEmptyValue, boolean allowAnyValue) {
+ super(option, allowedValues, allowEmptyValue, allowAnyValue);
+ }
+
+ @Override
+ public String getString() {
+ return option().pendingValue();
+ }
+
+ @Override
+ public void setFromString(String value) {
+ option().requestSet(getValidValue(value));
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public AbstractWidget provideWidget(YACLScreen screen, Dimension<Integer> widgetDimension) {
+ return new DropdownStringControllerElement(this, screen, widgetDimension);
+ }
+
+}
diff --git a/common/src/main/java/dev/isxander/yacl3/gui/controllers/dropdown/DropdownStringControllerElement.java b/common/src/main/java/dev/isxander/yacl3/gui/controllers/dropdown/DropdownStringControllerElement.java
new file mode 100644
index 0000000..615aada
--- /dev/null
+++ b/common/src/main/java/dev/isxander/yacl3/gui/controllers/dropdown/DropdownStringControllerElement.java
@@ -0,0 +1,31 @@
+package dev.isxander.yacl3.gui.controllers.dropdown;
+
+import dev.isxander.yacl3.api.utils.Dimension;
+import dev.isxander.yacl3.gui.YACLScreen;
+
+import java.util.List;
+
+public class DropdownStringControllerElement extends AbstractDropdownControllerElement<String, String> {
+ private final DropdownStringController controller;
+
+ public DropdownStringControllerElement(DropdownStringController control, YACLScreen screen, Dimension<Integer> dim) {
+ super(control, screen, dim);
+ this.controller = control;
+ }
+
+ @Override
+ public List<String> computeMatchingValues() {
+ return controller.getAllowedValues(inputField).stream()
+ .filter(this::matchingValue)
+ .sorted((s1, s2) -> {
+ if (s1.startsWith(inputField) && !s2.startsWith(inputField)) return -1;
+ if (!s1.startsWith(inputField) && s2.startsWith(inputField)) return 1;
+ return s1.compareTo(s2);
+ })
+ .toList();
+ }
+
+ public String getString(String object) {
+ return object;
+ }
+}
diff --git a/common/src/main/java/dev/isxander/yacl3/gui/controllers/dropdown/ItemController.java b/common/src/main/java/dev/isxander/yacl3/gui/controllers/dropdown/ItemController.java
new file mode 100644
index 0000000..ac903c7
--- /dev/null
+++ b/common/src/main/java/dev/isxander/yacl3/gui/controllers/dropdown/ItemController.java
@@ -0,0 +1,68 @@
+package dev.isxander.yacl3.gui.controllers.dropdown;
+
+import dev.isxander.yacl3.api.Option;
+import dev.isxander.yacl3.api.utils.Dimension;
+import dev.isxander.yacl3.gui.AbstractWidget;
+import dev.isxander.yacl3.gui.YACLScreen;
+import dev.isxander.yacl3.gui.utils.ItemRegistryHelper;
+import net.minecraft.core.registries.BuiltInRegistries;
+import net.minecraft.network.chat.Component;
+import net.minecraft.resources.ResourceLocation;
+import net.minecraft.world.item.Item;
+
+/**
+ * Simple controller that simply runs the button action on press
+ * and renders a {@link} Text on the right.
+ */
+public class ItemController extends AbstractDropdownController<Item> {
+
+ /**
+ * Constructs an item controller
+ *
+ * @param option bound option
+ */
+ public ItemController(Option<Item> option) {
+ super(option);
+ }
+
+ @Override
+ public String getString() {
+ return BuiltInRegistries.ITEM.getKey(option.pendingValue()).toString();
+ }
+
+ @Override
+ public void setFromString(String value) {
+ option.requestSet(ItemRegistryHelper.getItemFromName(value, option.pendingValue()));
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Component formatValue() {
+ return Component.literal(getString());
+ }
+
+
+ @Override
+ public boolean isValueValid(String value) {
+ return ItemRegistryHelper.isRegisteredItem(value);
+ }
+
+ @Override
+ protected String getValidValue(String value, int offset) {
+ return ItemRegistryHelper.getMatchingItemIdentifiers(value)
+ .skip(offset)