aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/client/java/dev/isxander/yacl/api/ConfigCategory.java9
-rw-r--r--src/client/java/dev/isxander/yacl/api/ListOption.java217
-rw-r--r--src/client/java/dev/isxander/yacl/api/ListOptionEntry.java23
-rw-r--r--src/client/java/dev/isxander/yacl/api/Option.java10
-rw-r--r--src/client/java/dev/isxander/yacl/api/OptionGroup.java8
-rw-r--r--src/client/java/dev/isxander/yacl/api/utils/OptionUtils.java14
-rw-r--r--src/client/java/dev/isxander/yacl/gui/CategoryListWidget.java6
-rw-r--r--src/client/java/dev/isxander/yacl/gui/LowProfileButtonWidget.java2
-rw-r--r--src/client/java/dev/isxander/yacl/gui/OptionListWidget.java207
-rw-r--r--src/client/java/dev/isxander/yacl/gui/TooltipButtonWidget.java11
-rw-r--r--src/client/java/dev/isxander/yacl/gui/controllers/ControllerWidget.java4
-rw-r--r--src/client/java/dev/isxander/yacl/gui/controllers/LabelController.java8
-rw-r--r--src/client/java/dev/isxander/yacl/gui/controllers/ListEntryWidget.java132
-rw-r--r--src/client/java/dev/isxander/yacl/gui/controllers/slider/SliderControllerElement.java6
-rw-r--r--src/client/java/dev/isxander/yacl/gui/controllers/string/StringControllerElement.java12
-rw-r--r--src/client/java/dev/isxander/yacl/gui/utils/GuiUtils.java3
-rw-r--r--src/client/java/dev/isxander/yacl/impl/ButtonOptionImpl.java5
-rw-r--r--src/client/java/dev/isxander/yacl/impl/ListOptionEntryImpl.java122
-rw-r--r--src/client/java/dev/isxander/yacl/impl/ListOptionImpl.java208
-rw-r--r--src/client/java/dev/isxander/yacl/impl/OptionGroupImpl.java2
-rw-r--r--src/client/java/dev/isxander/yacl/impl/OptionImpl.java5
-rw-r--r--src/client/java/dev/isxander/yacl/impl/YetAnotherConfigLibImpl.java67
-rw-r--r--src/main/resources/assets/yet-another-config-lib/lang/en_us.json5
-rw-r--r--src/testmod/java/dev/isxander/yacl/test/config/ExampleConfig.java4
-rw-r--r--src/testmod/java/dev/isxander/yacl/test/config/GuiTest.java33
25 files changed, 1047 insertions, 76 deletions
diff --git a/src/client/java/dev/isxander/yacl/api/ConfigCategory.java b/src/client/java/dev/isxander/yacl/api/ConfigCategory.java
index e9755dd..19c7f72 100644
--- a/src/client/java/dev/isxander/yacl/api/ConfigCategory.java
+++ b/src/client/java/dev/isxander/yacl/api/ConfigCategory.java
@@ -3,6 +3,7 @@ package dev.isxander.yacl.api;
import com.google.common.collect.ImmutableList;
import dev.isxander.yacl.impl.ConfigCategoryImpl;
import dev.isxander.yacl.impl.OptionGroupImpl;
+import dev.isxander.yacl.impl.utils.YACLConstants;
import net.minecraft.text.MutableText;
import net.minecraft.text.Text;
import org.apache.commons.lang3.Validate;
@@ -76,6 +77,11 @@ public interface ConfigCategory {
public Builder option(@NotNull Option<?> option) {
Validate.notNull(option, "`option` must not be null");
+ if (option instanceof ListOption<?> listOption) {
+ YACLConstants.LOGGER.warn("Adding list option as an option is not supported! Rerouting to group!");
+ return group(listOption);
+ }
+
this.rootOptions.add(option);
return this;
}
@@ -91,6 +97,9 @@ public interface ConfigCategory {
public Builder options(@NotNull Collection<Option<?>> options) {
Validate.notNull(options, "`options` must not be null");
+ if (options.stream().anyMatch(ListOption.class::isInstance))
+ throw new UnsupportedOperationException("List options must not be added as an option but a group!");
+
this.rootOptions.addAll(options);
return this;
}
diff --git a/src/client/java/dev/isxander/yacl/api/ListOption.java b/src/client/java/dev/isxander/yacl/api/ListOption.java
new file mode 100644
index 0000000..895898c
--- /dev/null
+++ b/src/client/java/dev/isxander/yacl/api/ListOption.java
@@ -0,0 +1,217 @@
+package dev.isxander.yacl.api;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import dev.isxander.yacl.impl.ListOptionImpl;
+import net.minecraft.text.MutableText;
+import net.minecraft.text.Text;
+import org.apache.commons.lang3.Validate;
+import org.jetbrains.annotations.ApiStatus;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.*;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+/**
+ * A list option that takes form as an option group for UX.
+ * You add this option through {@link ConfigCategory.Builder#group(OptionGroup)}. Do NOT add as an option.
+ * Users can add remove and reshuffle a list type. You can use any controller you wish, there are no dedicated
+ * controllers for list types. List options do not manipulate your list but get and set the list with a
+ * regular binding for simplicity.
+ *
+ * You may apply option flags like a normal option and collapse like a normal group, it is a merge of them both.
+ * Methods in this interface marked with {@link ApiStatus.Internal} should not be used, and could be subject to
+ * change at any time
+ * @param <T>
+ */
+public interface ListOption<T> extends OptionGroup, Option<List<T>> {
+ @Override
+ @NotNull ImmutableList<ListOptionEntry<T>> options();
+
+ /**
+ * Class of the entry type
+ */
+ @NotNull Class<T> elementTypeClass();
+
+ @ApiStatus.Internal
+ ListOptionEntry<T> insertNewEntryToTop();
+
+ @ApiStatus.Internal
+ void insertEntry(int index, ListOptionEntry<?> entry);
+
+ @ApiStatus.Internal
+ int indexOf(ListOptionEntry<?> entry);
+
+ @ApiStatus.Internal
+ void removeEntry(ListOptionEntry<?> entry);
+
+ @ApiStatus.Internal
+ void addRefreshListener(Runnable changedListener);
+
+ static <T> Builder<T> createBuilder(Class<T> typeClass) {
+ return new Builder<>(typeClass);
+ }
+
+ class Builder<T> {
+ private Text name = Text.empty();
+ private final List<Text> tooltipLines = new ArrayList<>();
+ private Function<ListOptionEntry<T>, Controller<T>> controllerFunction;
+ private Binding<List<T>> binding = null;
+ private final Set<OptionFlag> flags = new HashSet<>();
+ private T initialValue;
+ private boolean collapsed = false;
+ private boolean available = true;
+ private final Class<T> typeClass;
+
+ private Builder(Class<T> typeClass) {
+ this.typeClass = typeClass;
+ }
+
+ /**
+ * Sets name of the list, for UX purposes, a name should always be given,
+ * but isn't enforced.
+ *
+ * @see ListOption#name()
+ */
+ public Builder<T> name(@NotNull Text name) {
+ Validate.notNull(name, "`name` must not be null");
+
+ this.name = name;
+ return this;
+ }
+
+ /**
+ * Sets the tooltip to be used by the list. It is displayed like a normal
+ * group when you hover over the name. Entries do not allow a tooltip.
+ * <p>
+ * Can be invoked twice to append more lines.
+ * No need to wrap the text yourself, the gui does this itself.
+ *
+ * @param tooltips text lines - merged with a new-line on {@link Builder#build()}.
+ */
+ public Builder<T> tooltip(@NotNull Text... tooltips) {
+ Validate.notEmpty(tooltips, "`tooltips` cannot be empty");
+
+ tooltipLines.addAll(List.of(tooltips));
+ return this;
+ }
+
+ /**
+ * Sets the value that is used when creating new entries
+ */
+ public Builder<T> initial(@NotNull T initialValue) {
+ Validate.notNull(initialValue, "`initialValue` cannot be empty");
+
+ this.initialValue = initialValue;
+ return this;
+ }
+
+ /**
+ * Sets the controller for the option.
+ * This is how you interact and change the options.
+ *
+ * @see dev.isxander.yacl.gui.controllers
+ */
+ public Builder<T> controller(@NotNull Function<ListOptionEntry<T>, Controller<T>> control) {
+ Validate.notNull(control, "`control` cannot be null");
+
+ this.controllerFunction = control;
+ return this;
+ }
+
+ /**
+ * Sets the binding for the option.
+ * Used for default, getter and setter.
+ *
+ * @see Binding
+ */
+ public Builder<T> binding(@NotNull Binding<List<T>> binding) {
+ Validate.notNull(binding, "`binding` cannot be null");
+
+ this.binding = binding;
+ return this;
+ }
+
+ /**
+ * Sets the binding for the option.
+ * Shorthand of {@link Binding#generic(Object, Supplier, Consumer)}
+ *
+ * @param def default value of the option, used to reset
+ * @param getter should return the current value of the option
+ * @param setter should set the option to the supplied value
+ * @see Binding
+ */
+ public Builder<T> binding(@NotNull List<T> def, @NotNull Supplier<@NotNull List<T>> getter, @NotNull Consumer<@NotNull List<T>> setter) {
+ Validate.notNull(def, "`def` must not be null");
+ Validate.notNull(getter, "`getter` must not be null");
+ Validate.notNull(setter, "`setter` must not be null");
+
+ this.binding = Binding.generic(def, getter, setter);
+ return this;
+ }
+
+ /**
+ * Sets if the option can be configured
+ *
+ * @see Option#available()
+ */
+ public Builder<T> available(boolean available) {
+ this.available = available;
+ return this;
+ }
+
+ /**
+ * Adds a flag to the option.
+ * Upon applying changes, all flags are executed.
+ * {@link Option#flags()}
+ */
+ public Builder<T> flag(@NotNull OptionFlag... flag) {
+ Validate.notNull(flag, "`flag` must not be null");
+
+ this.flags.addAll(Arrays.asList(flag));
+ return this;
+ }
+
+ /**
+ * Adds a flag to the option.
+ * Upon applying changes, all flags are executed.
+ * {@link Option#flags()}
+ */
+ public Builder<T> flags(@NotNull Collection<OptionFlag> flags) {
+ Validate.notNull(flags, "`flags` must not be null");
+
+ this.flags.addAll(flags);
+ return this;
+ }
+
+ /**
+ * Dictates if the group should be collapsed by default.
+ * If not set, it will not be collapsed by default.
+ *
+ * @see OptionGroup#collapsed()
+ */
+ public Builder<T> collapsed(boolean collapsible) {
+ this.collapsed = collapsible;
+ return this;
+ }
+
+ public ListOption<T> build() {
+ Validate.notNull(controllerFunction, "`controller` must not be null");
+ Validate.notNull(binding, "`binding` must not be null");
+ Validate.notNull(initialValue, "`initialValue` must not be null");
+
+ MutableText concatenatedTooltip = Text.empty();
+ boolean first = true;
+ for (Text line : tooltipLines) {
+ if (!first) concatenatedTooltip.append("\n");
+ first = false;
+
+ concatenatedTooltip.append(line);
+ }
+
+ return new ListOptionImpl<>(name, concatenatedTooltip, binding, initialValue, typeClass, controllerFunction, ImmutableSet.copyOf(flags), collapsed, available);
+ }
+ }
+}
diff --git a/src/client/java/dev/isxander/yacl/api/ListOptionEntry.java b/src/client/java/dev/isxander/yacl/api/ListOptionEntry.java
new file mode 100644
index 0000000..e0a3424
--- /dev/null
+++ b/src/client/java/dev/isxander/yacl/api/ListOptionEntry.java
@@ -0,0 +1,23 @@
+package dev.isxander.yacl.api;
+
+import com.google.common.collect.ImmutableSet;
+import org.jetbrains.annotations.NotNull;
+
+public interface ListOptionEntry<T> extends Option<T> {
+ ListOption<T> parentGroup();
+
+ @Override
+ default @NotNull Class<T> typeClass() {
+ return parentGroup().elementTypeClass();
+ }
+
+ @Override
+ default @NotNull ImmutableSet<OptionFlag> flags() {
+ return parentGroup().flags();
+ }
+
+ @Override
+ default boolean available() {
+ return parentGroup().available();
+ }
+}
diff --git a/src/client/java/dev/isxander/yacl/api/Option.java b/src/client/java/dev/isxander/yacl/api/Option.java
index 772c816..394723f 100644
--- a/src/client/java/dev/isxander/yacl/api/Option.java
+++ b/src/client/java/dev/isxander/yacl/api/Option.java
@@ -71,12 +71,6 @@ public interface Option<T> {
boolean changed();
/**
- * If true, modifying this option recommends a restart.
- */
- @Deprecated
- boolean requiresRestart();
-
- /**
* Value in the GUI, ready to set the actual bound value or be undone.
*/
@NotNull T pendingValue();
@@ -109,6 +103,10 @@ public interface Option<T> {
*/
boolean isPendingValueDefault();
+ default boolean canResetToDefault() {
+ return true;
+ }
+
/**
* Adds a listener for when the pending value changes
*/
diff --git a/src/client/java/dev/isxander/yacl/api/OptionGroup.java b/src/client/java/dev/isxander/yacl/api/OptionGroup.java
index 3364bdf..6cc6c7f 100644
--- a/src/client/java/dev/isxander/yacl/api/OptionGroup.java
+++ b/src/client/java/dev/isxander/yacl/api/OptionGroup.java
@@ -31,7 +31,7 @@ public interface OptionGroup {
/**
* List of all options in the group
*/
- @NotNull ImmutableList<Option<?>> options();
+ @NotNull ImmutableList<? extends Option<?>> options();
/**
* Dictates if the group should be collapsed by default.
@@ -96,6 +96,9 @@ public interface OptionGroup {
public Builder option(@NotNull Option<?> option) {
Validate.notNull(option, "`option` must not be null");
+ if (option instanceof ListOption<?>)
+ throw new UnsupportedOperationException("List options must not be added as an option but a group!");
+
this.options.add(option);
return this;
}
@@ -109,6 +112,9 @@ public interface OptionGroup {
public Builder options(@NotNull Collection<? extends Option<?>> options) {
Validate.notEmpty(options, "`options` must not be empty");
+ if (options.stream().anyMatch(ListOption.class::isInstance))
+ throw new UnsupportedOperationException("List options must not be added as an option but a group!");
+
this.options.addAll(options);
return this;
}
diff --git a/src/client/java/dev/isxander/yacl/api/utils/OptionUtils.java b/src/client/java/dev/isxander/yacl/api/utils/OptionUtils.java
index ab46b5b..22032bd 100644
--- a/src/client/java/dev/isxander/yacl/api/utils/OptionUtils.java
+++ b/src/client/java/dev/isxander/yacl/api/utils/OptionUtils.java
@@ -1,9 +1,6 @@
package dev.isxander.yacl.api.utils;
-import dev.isxander.yacl.api.ConfigCategory;
-import dev.isxander.yacl.api.Option;
-import dev.isxander.yacl.api.OptionGroup;
-import dev.isxander.yacl.api.YetAnotherConfigLib;
+import dev.isxander.yacl.api.*;
import java.util.function.Consumer;
import java.util.function.Function;
@@ -16,9 +13,14 @@ public class OptionUtils {
public static void consumeOptions(YetAnotherConfigLib yacl, Function<Option<?>, Boolean> consumer) {
for (ConfigCategory category : yacl.categories()) {
for (OptionGroup group : category.groups()) {
- for (Option<?> option : group.options()) {
- if (consumer.apply(option)) return;
+ if (group instanceof ListOption<?> list) {
+ if (consumer.apply(list)) return;
+ } else {
+ for (Option<?> option : group.options()) {
+ if (consumer.apply(option)) return;
+ }
}
+
}
}
}
diff --git a/src/client/java/dev/isxander/yacl/gui/CategoryListWidget.java b/src/client/java/dev/isxander/yacl/gui/CategoryListWidget.java
index e97f46c..71a8e4e 100644
--- a/src/client/java/dev/isxander/yacl/gui/CategoryListWidget.java
+++ b/src/client/java/dev/isxander/yacl/gui/CategoryListWidget.java
@@ -32,12 +32,6 @@ public class CategoryListWidget extends ElementListWidgetExt<CategoryListWidget.
RenderSystem.disableScissor();
}
- public void postRender(MatrixStack matrices, int mouseX, int mouseY, float delta) {
- for (CategoryEntry entry : children()) {
- entry.postRender(matrices, mouseX, mouseY, delta);
- }
- }
-
@Override
public int getRowWidth() {
return Math.min(width - width / 10, 396);
diff --git a/src/client/java/dev/isxander/yacl/gui/LowProfileButtonWidget.java b/src/client/java/dev/isxander/yacl/gui/LowProfileButtonWidget.java
index 9fa01a7..36e0852 100644
--- a/src/client/java/dev/isxander/yacl/gui/LowProfileButtonWidget.java
+++ b/src/client/java/dev/isxander/yacl/gui/LowProfileButtonWidget.java
@@ -19,7 +19,7 @@ public class LowProfileButtonWidget extends ButtonWidget {
@Override
public void renderButton(MatrixStack matrices, int mouseX, int mouseY, float delta) {
- if (!isHovered()) {
+ if (!isHovered() || !active) {
int j = this.active ? 0xFFFFFF : 0xA0A0A0;
drawCenteredText(matrices, MinecraftClient.getInstance().textRenderer, this.getMessage(), this.getX() + this.width / 2, this.getY() + (this.height - 8) / 2, j | MathHelper.ceil(this.alpha * 255.0F) << 24);
} else {
diff --git a/src/client/java/dev/isxander/yacl/gui/OptionListWidget.java b/src/client/java/dev/isxander/yacl/gui/OptionListWidget.java
index 976d796..8284b0e 100644
--- a/src/client/java/dev/isxander/yacl/gui/OptionListWidget.java
+++ b/src/client/java/dev/isxander/yacl/gui/OptionListWidget.java
@@ -1,10 +1,9 @@
package dev.isxander.yacl.gui;
import com.google.common.collect.ImmutableList;
-import dev.isxander.yacl.api.ConfigCategory;
-import dev.isxander.yacl.api.Option;
-import dev.isxander.yacl.api.OptionGroup;
+import dev.isxander.yacl.api.*;
import dev.isxander.yacl.api.utils.Dimension;
+import dev.isxander.yacl.gui.controllers.ListEntryWidget;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.font.MultilineText;
import net.minecraft.client.font.TextRenderer;
@@ -13,14 +12,11 @@ import net.minecraft.client.gui.Selectable;
import net.minecraft.client.gui.screen.Screen;
import net.minecraft.client.gui.screen.narration.NarrationMessageBuilder;
import net.minecraft.client.gui.screen.narration.NarrationPart;
-import net.minecraft.client.gui.widget.ElementListWidget;
import net.minecraft.client.util.math.MatrixStack;
import net.minecraft.text.Text;
-import net.minecraft.util.math.MathHelper;
import org.jetbrains.annotations.Nullable;
import java.util.*;
-import java.util.function.Supplier;
public class OptionListWidget extends ElementListWidgetExt<OptionListWidget.Entry> {
private final YACLScreen yaclScreen;
@@ -33,6 +29,14 @@ public class OptionListWidget extends ElementListWidgetExt<OptionListWidget.Entr
this.yaclScreen = screen;
refreshOptions();
+
+ for (ConfigCategory category : screen.config.categories()) {
+ for (OptionGroup group : category.groups()) {
+ if (group instanceof ListOption<?> listOption) {
+ listOption.addRefreshListener(() -> refreshListEntries(listOption, category));
+ }
+ }
+ }
}
public void refreshOptions() {
@@ -40,6 +44,7 @@ public class OptionListWidget extends ElementListWidgetExt<OptionListWidget.Entr
List<ConfigCategory> categories = new ArrayList<>();
if (yaclScreen.getCurrentCategoryIdx() == -1) {
+ // -1 = no category, search in progress, so use all categories for search
categories.addAll(yaclScreen.config.categories());
} else {
categories.add(yaclScreen.config.categories().get(yaclScreen.getCurrentCategoryIdx()));
@@ -48,19 +53,19 @@ public class OptionListWidget extends ElementListWidgetExt<OptionListWidget.Entr
for (ConfigCategory category : categories) {
for (OptionGroup group : category.groups()) {
- Supplier<Boolean> viewableSupplier;
- GroupSeparatorEntry groupSeparatorEntry = null;
+ GroupSeparatorEntry groupSeparatorEntry;
if (!group.isRoot()) {
- groupSeparatorEntry = new GroupSeparatorEntry(group, yaclScreen);
- viewableSupplier = groupSeparatorEntry::isExpanded;
+ groupSeparatorEntry = group instanceof ListOption<?> listOption
+ ? new ListGroupSeparatorEntry(listOption, yaclScreen)
+ : new GroupSeparatorEntry(group, yaclScreen);
addEntry(groupSeparatorEntry);
} else {
- viewableSupplier = () -> true;
+ groupSeparatorEntry = null;
}
List<OptionEntry> optionEntries = new ArrayList<>();
for (Option<?> option : group.options()) {
- OptionEntry entry = new OptionEntry(option, category, group, option.controller().provideWidget(yaclScreen, Dimension.ofInt(getRowLeft(), 0, getRowWidth(), 20)), viewableSupplier);
+ OptionEntry entry = new OptionEntry(option, category, group, groupSeparatorEntry, option.controller().provideWidget(yaclScreen, getDefaultEntryPosition()));
addEntry(entry);
optionEntries.add(entry);
}
@@ -76,6 +81,30 @@ public class OptionListWidget extends ElementListWidgetExt<OptionListWidget.Entr
resetSmoothScrolling();
}
+ private void refreshListEntries(ListOption<?> listOption, ConfigCategory category) {
+ // find group separator for group
+ GroupSeparatorEntry groupSeparator = super.children().stream().filter(e -> e instanceof GroupSeparatorEntry gs && gs.group == listOption).map(GroupSeparatorEntry.class::cast).findAny().orElse(null);
+
+ if (groupSeparator == null)
+ return;
+
+ for (OptionEntry entry : groupSeparator.optionEntries)
+ super.removeEntry(entry);
+
+ groupSeparator.optionEntries.clear();
+ Entry lastEntry = groupSeparator;
+ for (ListOptionEntry<?> listOptionEntry : listOption.options()) {
+ OptionEntry optionEntry = new OptionEntry(listOptionEntry, category, listOption, groupSeparator, listOptionEntry.controller().provideWidget(yaclScreen, getDefaultEntryPosition()));
+ addEntryBelow(lastEntry, optionEntry);
+ groupSeparator.optionEntries.add(optionEntry);
+ lastEntry = optionEntry;
+ }
+ }
+
+ public Dimension<Integer> getDefaultEntryPosition() {
+ return Dimension.ofInt(getRowLeft(), 0, getRowWidth(), 20);
+ }
+
public void expandAllGroups() {
for (Entry entry : super.children()) {
if (entry instanceof GroupSeparatorEntry groupSeparatorEntry) {
@@ -153,6 +182,40 @@ public class OptionListWidget extends ElementListWidgetExt<OptionListWidget.Entr
return viewableChildren;
}
+ public void addEntry(int index, Entry entry) {
+ super.children().add(index, entry);
+ recacheViewableChildren();
+ }
+
+ public void addEntryBelow(Entry below, Entry entry) {
+ int idx = super.children().indexOf(below) + 1;
+
+ if (idx == 0)
+ throw new IllegalStateException("The entry to insert below does not exist!");
+
+ addEntry(idx, entry);
+ }
+
+ public void addEntryBelowWithoutScroll(Entry below, Entry entry) {
+ double d = (double)this.getMaxScroll() - this.getScrollAmount();
+ addEntryBelow(below, entry);
+ setScrollAmount(getMaxScroll() - d);
+ }
+
+ @Override
+ public boolean removeEntryWithoutScrolling(Entry entry) {
+ boolean ret = super.removeEntryWithoutScrolling(entry);
+ recacheViewableChildren();
+ return ret;
+ }
+
+ @Override
+ public boolean removeEntry(Entry entry) {
+ boolean ret = super.removeEntry(entry);
+ recacheViewableChildren();
+ return ret;
+ }
+
public abstract class Entry extends ElementListWidgetExt.Entry<Entry> {
public boolean isViewable() {
return true;
@@ -168,25 +231,28 @@ public class OptionListWidget extends ElementListWidgetExt<OptionListWidget.Entr
public final ConfigCategory category;
public final OptionGroup group;
+ public final @Nullable GroupSeparatorEntry groupSeparatorEntry;
+
public final AbstractWidget widget;
- private final Supplier<Boolean> viewableSupplier;
private final TextScaledButtonWidget resetButton;
private final String categoryName;
private final String groupName;
- private OptionEntry(Option<?> option, ConfigCategory category, OptionGroup group, AbstractWidget widget, Supplier<Boolean> viewableSupplier) {
+ public OptionEntry(Option<?> option, ConfigCategory category, OptionGroup group, @Nullable GroupSeparatorEntry groupSeparatorEntry, AbstractWidget widget) {
this.option = option;
this.category = category;
this.group = group;
- this.widget = widget;
- this.viewableSupplier = viewableSupplier;
+ this.groupSeparatorEntry = groupSeparatorEntry;
+ if (option instanceof ListOptionEntry<?> listOptionEntry)
+ this.widget = new ListEntryWidget(yaclScreen, listOptionEntry, widget);
+ else this.widget = widget;
this.categoryName = category.name().getString().toLowerCase();
this.groupName = group.name().getString().toLowerCase();
- if (this.widget.canReset()) {
- this.widget.setDimension(this.widget.getDimension().expanded(-21, 0));
- this.resetButton = new TextScaledButtonWidget(widget.getDimension().xLimit() + 1, -50, 20, 20, 2f, Text.of("\u21BB"), button -> {
+ if (option.canResetToDefault() && this.widget.canReset()) {
+ this.widget.setDimension(this.widget.getDimension().expanded(-20, 0));
+ this.resetButton = new TextScaledButtonWidget(widget.getDimension().xLimit(), -50, 20, 20, 2f, Text.of("\u21BB"), button -> {
option.requestSetDefault();
});
option.addListener((opt, val) -> this.resetButton.active = !opt.isPendingValueDefault() && opt.available());
@@ -231,7 +297,7 @@ public class OptionListWidget extends ElementListWidgetExt<OptionListWidget.Entr
@Override
public boolean isViewable() {
String query = yaclScreen.searchFieldWidget.getText();
- return viewableSupplier.get()
+ return (groupSeparatorEntry == null || groupSeparatorEntry.isExpanded())
&& (yaclScreen.searchFieldWidget.isEmpty()
|| (!singleCategory && categoryName.contains(query))
|| groupName.contains(query)
@@ -261,18 +327,18 @@ public class OptionListWidget extends ElementListWidgetExt<OptionListWidget.Entr
}
public class GroupSeparatorEntry extends Entry {
- private final OptionGroup group;
- private final MultilineText wrappedName;
- private final MultilineText wrappedTooltip;
+ protected final OptionGroup group;
+ protected final MultilineText wrappedName;
+ protected final MultilineText wrappedTooltip;
- private final LowProfileButtonWidget expandMinimizeButton;
+ protected final LowProfileButtonWidget expandMinimizeButton;
- private final Screen screen;
- private final TextRenderer textRenderer = MinecraftClient.getInstance().textRenderer;
+ protected final Screen screen;
+ protected final TextRenderer textRenderer = MinecraftClient.getInstance().textRenderer;
- private boolean groupExpanded;
+ protected boolean groupExpanded;
- private List<OptionEntry> optionEntries;
+ protected List<OptionEntry> optionEntries;
private int y;
@@ -282,10 +348,7 @@ public class OptionListWidget extends ElementListWidgetExt<OptionListWidget.Entr
this.wrappedName = MultilineText.create(textRenderer, group.name(), getRowWidth() - 45);
this.wrappedTooltip = MultilineText.create(textRenderer, group.tooltip(), screen.width / 3 * 2 - 10);
this.groupExpanded = !group.collapsed();
- this.expandMinimizeButton = new LowProfileButtonWidget(0, 0, 20, 20, Text.empty(), btn -> {
- setExpanded(!isExpanded());
- recacheViewableChildren();
- });
+ this.expandMinimizeButton = new LowProfileButtonWidget(0, 0, 20, 20, Text.empty(), btn -> onExpandButtonPress());
updateExpandMinimizeText();
}
@@ -293,8 +356,10 @@ public class OptionListWidget extends ElementListWidgetExt<OptionListWidget.Entr
public void render(MatrixStack matrices, int index, int y, int x, int entryWidth, int entryHeight, int mouseX, int mouseY, boolean hovered, float tickDelta) {
this.y = y;
+ int buttonY = y + entryHeight / 2 - expandMinimizeButton.getHeight() / 2 + 1;
+
+ expandMinimizeButton.setY(buttonY);
expandMinimizeButton.setX(x);
- expandMinimizeButton.setY(y + entryHeight / 2 - expandMinimizeButton.getHeight() / 2);
expandMinimizeButton.render(matrices, mouseX, mouseY, tickDelta);
wrappedName.drawCenterWithShadow(matrices, x + entryWidth / 2, y + getYPadding());
@@ -316,7 +381,12 @@ public class OptionListWidget extends ElementListWidgetExt<OptionListWidget.Entr
updateExpandMinimizeText();
}
- private void updateExpandMinimizeText() {
+ protected void onExpandButtonPress() {
+ setExpanded(!isExpanded());
+ recacheViewableChildren();
+ }
+
+ protected void updateExpandMinimizeText() {
expandMinimizeButton.setMessage(Text.of(isExpanded() ? "▼" : "▶"));
}
@@ -349,6 +419,7 @@ public class OptionListWidget extends ElementListWidgetExt<OptionListWidget.Entr
@Override
public void appendNarrations(NarrationMessageBuilder builder) {
builder.put(NarrationPart.TITLE, group.name());
+ builder.put(NarrationPart.HINT, group.tooltip());
}
});
}
@@ -358,4 +429,72 @@ public class OptionListWidget extends ElementListWidgetExt<OptionListWidget.Entr
return ImmutableList.of(expandMinimizeButton);
}
}
+
+ public class ListGroupSeparatorEntry extends GroupSeparatorEntry {
+ private final ListOption<?> listOption;
+ private final TextScaledButtonWidget resetListButton;
+ private final TooltipButtonWidget addListButton;
+
+ private ListGroupSeparatorEntry(ListOption<?> group, Screen screen) {
+ super(group, screen);
+ this.listOption = group;
+
+ this.resetListButton = new TextScaledButtonWidget(getRowRight() - 20, -50, 20, 20, 2f, Text.of("\u21BB"), button -> {
+ group.requestSetDefault();
+ });
+ group.addListener((opt, val) -> this.resetListButton.active = !opt.isPendingValueDefault() && opt.available());
+ this.resetListButton.active = !group.isPendingValueDefault() && group.available();
+
+ this.addListButton = new TooltipButtonWidget(yaclScreen, resetListButton.getX() - 20, -50, 20, 20, Text.of("+"), Text.translatable("yacl.list.add_top"), btn -> {
+ group.insertNewEntryToTop();
+ });
+
+ updateExpandMinimizeText();
+ minimizeIfUnavailable();
+ }
+
+ @Override
+ public void render(MatrixStack matrices, int index, int y, int x, int entryWidth, int entryHeight, int mouseX, int mouseY, boolean hovered, float tickDelta) {
+ updateExpandMinimizeText(); // update every render because option could become available/unavailable at any time
+
+ super.render(matrices, index, y, x, entryWidth, entryHeight, mouseX, mouseY, hovered, tickDelta);
+
+ int buttonY = expandMinimizeButton.getY();
+
+ resetListButton.setY(buttonY);
+ addListButton.setY(buttonY);
+
+ resetListButton.render(matrices, mouseX, mouseY, tickDelta);
+ addListButton.render(matrices, mouseX, mouseY, tickDelta);
+ }
+
+ @Override
+ public void postRender(MatrixStack matrices, int mouseX, int mouseY, float delta) {
+ minimizeIfUnavailable(); // cannot run in render because it *should* cause a ConcurrentModificationException (but doesn't)
+
+ super.postRender(matrices, mouseX, mouseY, delta);
+
+ addListButton.renderHoveredTooltip(matrices);
+ }
+
+ private void minimizeIfUnavailable() {
+ if (!listOption.available() && isExpanded()) {
+ setExpanded(false);
+ recacheViewableChildren();
+ }
+ }
+
+ @Override
+ protected void updateExpandMinimizeText() {
+ super.updateExpandMinimizeText();
+ if (resetListButton != null && addListButton != null)
+ resetListButton.visible = addListButton.visible = isExpanded();
+ expandMinimizeButton.active = listOption == null || listOption.available();
+ }
+
+ @Override
+ public List<? extends Element> children() {
+ return ImmutableList.of(expandMinimizeButton, addListButton, resetListButton);
+ }
+ }
}
diff --git a/src/client/java/dev/isxander/yacl/gui/TooltipButtonWidget.java b/src/client/java/dev/isxander/yacl/gui/TooltipButtonWidget.java
index b034f4b..359e8d0 100644
--- a/src/client/java/dev/isxander/yacl/gui/TooltipButtonWidget.java
+++ b/src/client/java/dev/isxander/yacl/gui/TooltipButtonWidget.java
@@ -3,21 +3,26 @@ package dev.isxander.yacl.gui;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.font.MultilineText;
import net.minecraft.client.gui.screen.Screen;
+import net.minecraft.client.gui.tooltip.Tooltip;
import net.minecraft.client.gui.widget.ButtonWidget;
import net.minecraft.client.util.math.MatrixStack;
import net.minecraft.text.Text;
-public class TooltipButtonWidget extends ButtonWidget {
+public class TooltipButtonWidget extends TextScaledButtonWidget {
protected final Screen screen;
protected MultilineText wrappedDescription;
- public TooltipButtonWidget(Screen screen, int x, int y, int width, int height, Text message, Text tooltip, PressAction onPress) {
- super(x, y, width, height, message, onPress, DEFAULT_NARRATION_SUPPLIER);
+ public TooltipButtonWidget(Screen screen, int x, int y, int width, int height, Text message, float textScale, Text tooltip, PressAction onPress) {
+ super(x, y, width, height, textScale, message, onPress);
this.screen = screen;
setTooltip(tooltip);
}
+ public TooltipButtonWidget(Screen screen, int x, int y, int width, int height, Text message, Text tooltip, PressAction onPress) {
+ this(screen, x, y, width, height, message, 1, tooltip, onPress);
+ }
+
public void renderHoveredTooltip(MatrixStack matrices) {
if (isHovered()) {
YACLScreen.renderMultilineTooltip(matrices, MinecraftClient.getInstance().textRenderer, wrappedDescription, getX() + width / 2, getY() - 4, getY() + height + 4, screen.width, screen.height);
diff --git a/src/client/java/dev/isxander/yacl/gui/controllers/ControllerWidget.java b/src/client/java/dev/isxander/yacl/gui/controllers/ControllerWidget.java
index 0f49d3d..ae54ca4 100644
--- a/src/client/java/dev/isxander/yacl/gui/controllers/ControllerWidget.java
+++ b/src/client/java/dev/isxander/yacl/gui/controllers/ControllerWidget.java
@@ -8,6 +8,7 @@ import dev.isxander.yacl.gui.utils.GuiUtils;
import net.minecraft.client.font.MultilineText;
import net.minecraft.client.gui.DrawableHelper;
import net.minecraft.client.gui.screen.narration.NarrationMessageBuilder;
+import net.minecraft.client.gui.screen.narration.NarrationPart;
import net.minecraft.client.util.math.MatrixStack;
import net.minecraft.text.Text;
import net.minecraft.util.Formatting;
@@ -151,6 +152,7 @@ public abstract class ControllerWidget<T extends Controller<?>> extends Abstract
@Override
public void appendNarrations(NarrationMessageBuilder builder) {
-
+ builder.put(NarrationPart.TITLE, control.option().name());
+ builder.put(NarrationPart.HINT, control.option().tooltip());
}
}
diff --git a/src/client/java/dev/isxander/yacl/gui/controllers/LabelController.java b/src/client/java/dev/isxander/yacl/gui/controllers/LabelController.java
index 8369680..960c950 100644
--- a/src/client/java/dev/isxander/yacl/gui/controllers/LabelController.java
+++ b/src/client/java/dev/isxander/yacl/gui/controllers/LabelController.java
@@ -64,7 +64,7 @@ public class LabelController implements Controller<Text> {
float y = getDimension().y();
for (OrderedText text : wrappedText) {
- textRenderer.drawWithShadow(matrices, text, getDimension().x(), y + getYPadding(), option().available() ? -1 : 0xFFA0A0A0);
+ textRenderer.drawWithShadow(matrices, text, getDimension().x() + getXPadding(), y + getYPadding(), option().available() ? -1 : 0xFFA0A0A0);
y += textRenderer.fontHeight;
}
}
@@ -123,12 +123,16 @@ public class LabelController implements Controller<Text> {
return textRenderer.getTextHandler().getStyleAt(wrappedText.get(line), x);
}
+ private int getXPadding() {
+ return 4;
+ }
+
private int getYPadding() {
return 3;
}
private void updateText() {
- wrappedText = textRenderer.wrapLines(formatValue(), getDimension().width());
+ wrappedText = textRenderer.wrapLines(formatValue(), getDimension().width() - getXPadding() * 2);
setDimension(getDimension().withHeight(wrappedText.size() * textRenderer.fontHeight + getYPadding() * 2));
}
diff --git a/src/client/java/dev/isxander/yacl/gui/controllers/ListEntryWidget.java b/src/client/java/dev/isxander/yacl/gui/controllers/ListEntryWidget.java
new file mode 100644
index 0000000..a548efb
--- /dev/null
+++ b/src/client/java/dev/isxander/yacl/gui/controllers/ListEntryWidget.java
@@ -0,0 +1,132 @@
+package dev.isxander.yacl.gui.controllers;
+
+import com.google.common.collect.ImmutableList;
+import dev.isxander.yacl.api.ListOption;
+import dev.isxander.yacl.api.ListOptionEntry;
+import dev.isxander.yacl.api.utils.Dimension;
+import dev.isxander.yacl.gui.*;
+import net.minecraft.client.gui.Element;
+import net.minecraft.client.gui.ParentElement;
+import net.minecraft.client.gui.screen.narration.NarrationMessageBuilder;
+import net.minecraft.client.util.math.MatrixStack;
+import net.minecraft.text.Text;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.List;
+
+public class ListEntryWidget extends AbstractWidget implements ParentElement {
+ private final TooltipButtonWidget removeButton, moveUpButton, moveDownButton;
+ private final AbstractWidget entryWidget;
+
+ private final ListOption<?> listOption;
+ private final ListOptionEntry<?> listOptionEntry;
+
+ private final String optionNameString;
+
+ private Element focused;
+ private boolean dragging;
+
+ public ListEntryWidget(YACLScreen screen, ListOptionEntry<?> listOptionEntry, AbstractWidget entryWidget) {
+ super(entryWidget.getDimension());
+ this.listOptionEntry = listOptionEntry;
+ this.listOption = listOptionEntry.parentGroup();
+ this.optionNameString = listOptionEntry.name().getString().toLowerCase();
+ this.entryWidget = entryWidget;
+
+ Dimension<Integer> dim = entryWidget.getDimension();
+ entryWidget.setDimension(dim.clone().move(20 * 2, 0).expand(-20 * 3, 0));
+
+ removeButton = new TooltipButtonWidget(screen, dim.xLimit() - 20, dim.y(), 20, 20, Text.of("\u274c"), Text.translatable("yacl.list.remove"), btn -> {
+ listOption.removeEntry(listOptionEntry);
+ });
+
+ moveUpButton = new TooltipButtonWidget(screen, dim.x(), dim.y(), 20, 20, Text.of("\u2191"), Text.translatable("yacl.list.move_up"), btn -> {
+ int index = listOption.indexOf(listOptionEntry) - 1;
+ if (index >= 0) {
+ listOption.removeEntry(listOptionEntry);
+ listOption.insertEntry(index, listOptionEntry);
+ updateButtonStates();
+ }
+ });
+
+ moveDownButton = new TooltipButtonWidget(screen, dim.x() + 20, dim.y(), 20, 20, Text.of("\u2193"), Text.translatable("yacl.list.move_down"), btn -> {
+ int index = listOption.indexOf(listOptionEntry) + 1;
+ if (index < listOption.options().size()) {
+ listOption.removeEntry(listOptionEntry);
+ listOption.insertEntry(index, listOptionEntry);
+ updateButtonStates();
+ }
+ });
+
+ updateButtonStates();
+ }
+
+ @Override
+ public void render(MatrixStack matrices, int mouseX, int mouseY, float delta) {
+ updateButtonStates(); // update every render in case option becomes available/unavailable
+
+ removeButton.setY(getDimension().y());
+ moveUpButton.setY(getDimension().y());
+ moveDownButton.setY(getDimension().y());
+ entryWidget.setDimension(entryWidget.getDimension().withY(getDimension().y()));
+
+ removeButton.render(matrices, mouseX, mouseY, delta);
+ moveUpButton.render(matrices, mouseX, mouseY, delta);
+ moveDownButton.render(matrices, mouseX, mouseY, delta);
+ entryWidget.render(matrices, mouseX, mouseY, delta);
+ }
+
+ @Override
+ public void postRender(MatrixStack matrices, int mouseX, int mouseY, float delta) {
+ removeButton.renderHoveredTooltip(matrices);
+ moveUpButton.renderHoveredTooltip(matrices);
+ moveDownButton.renderHoveredTooltip(matrices);
+ }
+
+ protected void updateButtonStates() {
+ removeButton.active = listOption.available();
+ moveUpButton.active = listOption.indexOf(listOptionEntry) > 0 && listOption.available();
+ moveDownButton.active = listOption.indexOf(listOptionEntry) < listOption.options().size() - 1 && listOption.available();
+ }
+
+ @Override
+ public void unfocus() {
+ entryWidget.unfocus();
+ }
+
+ @Override
+ public void appendNarrations(NarrationMessageBuilder builder) {
+ entryWidget.appendNarrations(builder);
+ }
+
+ @Override
+ public boolean matchesSearch(String query) {
+ return optionNameString.contains(query.toLowerCase());
+ }
+
+ @Override
+ public List<? extends Element> children() {
+ return ImmutableList.of(moveUpButton, moveDownButton, entryWidget, removeButton);
+ }
+
+ @Override
+ public boolean isDragging() {
+ return dragging;
+ }
+
+ @Override
+ public void setDragging(boolean dragging) {
+ this.dragging = dragging;
+ }
+
+ @Nullable
+ @Override
+ public Element getFocused() {
+ return focused;
+ }
+
+ @Override
+ public void setFocused(@Nullable Element focused) {
+ this.focused = focused;
+ }
+}
diff --git a/src/client/java/dev/isxander/yacl/gui/controllers/slider/SliderControllerElement.java b/src/client/java/dev/isxander/yacl/gui/controllers/slider/SliderControllerElement.java
index c78e0eb..ea4e262 100644
--- a/src/client/java/dev/isxander/yacl/gui/controllers/slider/SliderControllerElement.java
+++ b/src/client/java/dev/isxander/yacl/gui/controllers/slider/SliderControllerElement.java
@@ -141,7 +141,11 @@ public class SliderControllerElement extends ControllerWidget<ISliderController<
@Override
public void setDimension(Dimension<Integer> dim) {
super.setDimension(dim);
- sliderBounds = Dimension.ofInt(dim.xLimit() - getXPadding() - getThumbWidth() / 2 - dim.width() / 3, dim.centerY() - 5, dim.width() / 3, 10);
+ int trackWidth = dim.width() / 3;
+ if (optionNameString.isEmpty())
+ trackWidth = dim.width() / 2;
+
+ sliderBounds = Dimension.ofInt(dim.xLimit() - getXPadding() - getThumbWidth() / 2 - trackWidth, dim.centerY() - 5, trackWidth, 10);
}
protected int getThumbX() {
diff --git a/src/client/java/dev/isxander/yacl/gui/controllers/string/StringControllerElement.java b/src/client/java/dev/isxander/yacl/gui/controllers/string/StringControllerElement.java
index ef70341..da33aec 100644
--- a/src/client/java/dev/isxander/yacl/gui/controllers/string/StringControllerElement.java
+++ b/src/client/java/dev/isxander/yacl/gui/controllers/string/StringControllerElement.java
@@ -49,7 +49,7 @@ public class StringControllerElement extends ControllerWidget<IStringController<
@Override
protected void drawValueText(MatrixStack matrices, int mouseX, int mouseY, float delta) {
Text valueText = getValueText();
- if (!isHovered()) valueText = Text.literal(GuiUtils.shortenString(valueText.getString(), textRenderer, getDimension().width() / 2, "...")).setStyle(valueText.getStyle());
+ if (!isHovered()) valueText = Text.literal(GuiUtils.shortenString(valueText.getString(), textRenderer, getMaxUnwrapLength(), "...")).setStyle(valueText.getStyle());
matrices.push();
int textX = getDimension().xLimit() - textRenderer.getWidth(valueText) + renderOffset - getXPadding();
@@ -311,9 +311,17 @@ public class StringControllerElement extends ControllerWidget<IStringController<
}
public int getUnshiftedLength() {
+ if (optionNameString.isEmpty())
+ return getDimension().width() - getXPadding() * 2;
return getDimension().width() / 8 * 5;
}
+ public int getMaxUnwrapLength() {
+ if (optionNameString.isEmpty())
+ return getDimension().width() - getXPadding() * 2;
+ return getDimension().width() / 2;
+ }
+
public int getSelectionStart() {
return Math.min(caretPos, caretPos + selectionLength);
}
@@ -378,7 +386,7 @@ public class StringControllerElement extends ControllerWidget<IStringController<
@Override
protected int getUnhoveredControlWidth() {
- return !isHovered() ? Math.min(getHoveredControlWidth(), getDimension().width() / 2) : getHoveredControlWidth();
+ return !isHovered() ? Math.min(getHoveredControlWidth(), getMaxUnwrapLength()) : getHoveredControlWidth();
}
@Override
diff --git a/src/client/java/dev/isxander/yacl/gui/utils/GuiUtils.java b/src/client/java/dev/isxander/yacl/gui/utils/GuiUtils.java
index 670bd12..b1f0148 100644
--- a/src/client/java/dev/isxander/yacl/gui/utils/GuiUtils.java
+++ b/src/client/java/dev/isxander/yacl/gui/utils/GuiUtils.java
@@ -22,6 +22,9 @@ public class GuiUtils {
}
public static String shortenString(String string, TextRenderer textRenderer, int maxWidth, String suffix) {
+ if (string.isEmpty())
+ return string;
+
boolean firstIter = true;
while (textRenderer.getWidth(string) > maxWidth) {
string = string.substring(0, Math.max(string.length() - 1 - (firstIter ? 1 : suffix.length() + 1), 0)).trim();
diff --git a/src/client/java/dev/isxander/yacl/impl/ButtonOptionImpl.java b/src/client/java/dev/isxander/yacl/impl/ButtonOptionImpl.java
index dcb9c7a..f526d42 100644
--- a/src/client/java/dev/isxander/yacl/impl/ButtonOptionImpl.java
+++ b/src/client/java/dev/isxander/yacl/impl/ButtonOptionImpl.java
@@ -79,11 +79,6 @@ public class ButtonOptionImpl implements ButtonOption {
}
@Override
- public boolean requiresRestart() {
- return false;
- }
-
- @Override
public boolean changed() {
return false;
}
diff --git a/src/client/java/dev/isxander/yacl/impl/ListOptionEntryImpl.java b/src/client/java/dev/isxander/yacl/impl/ListOptionEntryImpl.java
new file mode 100644
index 0000000..dc7aa88
--- /dev/null
+++ b/src/client/java/dev/isxander/yacl/impl/ListOptionEntryImpl.java
@@ -0,0 +1,122 @@
+package dev.isxander.yacl.impl;
+
+import dev.isxander.yacl.api.*;
+import net.minecraft.text.Text;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.function.BiConsumer;
+import java.util.function.Function;
+
+public class ListOptionEntryImpl<T> implements ListOptionEntry<T> {
+ private final ListOptionImpl<T> group;
+
+ private T value;
+
+ private final Binding<T> binding;
+ private final Controller<T> controller;
+
+ public ListOptionEntryImpl(ListOptionImpl<T> group, T initialValue, @NotNull Function<ListOptionEntry<T>, Controller<T>> controlGetter) {
+ this.group = group;
+ this.value = initialValue;
+ this.binding = new EntryBinding();
+ this.controller = controlGetter.apply(this);
+ }
+
+ @Override
+ public @NotNull Text name() {
+ return Text.empty();
+ }
+
+ @Override
+ public @NotNull Text tooltip() {
+ return Text.empty();
+ }
+
+ @Override
+ public @NotNull Controller<T> controller() {
+ return controller;
+ }
+
+ @Override
+ public @NotNull Binding<T> binding() {
+ return binding;
+ }
+
+ @Override
+ public boolean available() {
+ return parentGroup().available();
+ }
+
+ @Override
+ public void setAvailable(boolean available) {
+
+ }
+
+ @Override
+ public ListOption<T> parentGroup() {
+ return group;
+ }
+
+ @Override
+ public boolean changed() {
+ return false;
+ }
+
+ @Override
+ public @NotNull T pendingValue() {
+ return value;
+ }
+
+ @Override
+ public void requestSet(T value) {
+ binding.setValue(value);
+ }
+
+ @Override
+ public boolean applyValue() {
+ return false;
+ }
+
+ @Override
+ public void forgetPendingValue() {
+
+ }
+
+ @Override
+ public void requestSetDefault() {
+
+ }
+
+ @Override
+ public boolean isPendingValueDefault() {
+ return false;
+ }
+
+ @Override
+ public boolean canResetToDefault() {
+ return false;
+ }
+
+ @Override
+ public void addListener(BiConsumer<Option<T>, T> changedListener) {
+
+ }
+
+ private class EntryBinding implements Binding<T> {
+ @Override
+ public void setValue(T newValue) {
+ value = newValue;
+ group.callListeners();
+ }
+
+ @Override
+ public T getValue() {
+ return value;
+ }
+
+ @Override
+ public T defaultValue() {
+ throw new UnsupportedOperationException();
+ }
+ }
+}
diff --git a/src/client/java/dev/isxander/yacl/impl/ListOptionImpl.java b/src/client/java/dev/isxander/yacl/impl/ListOptionImpl.java
new file mode 100644
index 0000000..128d3e7
--- /dev/null
+++ b/src/client/java/dev/isxander/yacl/impl/ListOptionImpl.java
@@ -0,0 +1,208 @@
+package dev.isxander.yacl.impl;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import dev.isxander.yacl.api.*;
+import net.minecraft.text.Text;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.function.BiConsumer;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+public class ListOptionImpl<T> implements ListOption<T> {
+ private final Text name;
+ private final Text tooltip;
+ private final Binding<List<T>> binding;
+ private final T initialValue;
+ private final List<ListOptionEntry<T>> entries;
+ private final boolean collapsed;
+ private boolean available;
+ private final Class<T> typeClass;
+ private final ImmutableSet<OptionFlag> flags;
+ private final EntryFactory entryFactory;
+ private final List<BiConsumer<Option<List<T>>, List<T>>> listeners;
+ private final List<Runnable> refreshListeners;
+
+ public ListOptionImpl(@NotNull Text name, @NotNull Text tooltip, @NotNull Binding<List<T>> binding, @NotNull T initialValue, @NotNull Class<T> typeClass, @NotNull Function<ListOptionEntry<T>, Controller<T>> controllerFunction, ImmutableSet<OptionFlag> flags, boolean collapsed, boolean available) {
+ this.name = name;
+ this.tooltip = tooltip;
+ this.binding = binding;
+ this.initialValue = initialValue;
+ this.entryFactory = new EntryFactory(controllerFunction);
+ this.entries = createEntries(binding().getValue());
+ this.collapsed = collapsed;
+ this.typeClass = typeClass;
+ this.flags = flags;
+ this.available = available;
+ this.listeners = new ArrayList<>();
+ this.refreshListeners = new ArrayList<>();
+ callListeners();
+ }
+
+ @Override
+ public @NotNull Text name() {
+ return this.name;
+ }
+
+ @Override
+ public @NotNull Text tooltip() {
+ return this.tooltip;
+ }
+
+ @Override
+ public @NotNull ImmutableList<ListOptionEntry<T>> options() {
+ return ImmutableList.copyOf(entries);
+ }
+
+ @Override
+ public @NotNull Controller<List<T>> controller() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public @NotNull Binding<List<T>> binding() {
+ return binding;
+ }
+
+ @Override
+ public @NotNull Class<List<T>> typeClass() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public @NotNull Class<T> elementTypeClass() {
+ return typeClass;
+ }
+
+ @Override
+ public boolean collapsed() {
+ return collapsed;
+ }
+
+ @Override
+ public @NotNull ImmutableSet<OptionFlag> flags() {
+ return flags;
+ }
+
+ @Override
+ public ImmutableList<T> pendingValue() {
+ return ImmutableList.copyOf(entries.stream().map(Option::pendingValue).toList());
+ }
+
+ @Override
+ public void insertEntry(int index, ListOptionEntry<?> entry) {
+ entries.add(index, (ListOptionEntry<T>) entry);
+ onRefresh();
+ }
+
+ @Override
+ public ListOptionEntry<T> insertNewEntryToTop() {
+ ListOptionEntry<T> newEntry = entryFactory.create(initialValue);
+ entries.add(0, newEntry);
+ onRefresh();
+ return newEntry;
+ }
+
+ @Override
+ public void removeEntry(ListOptionEntry<?> entry) {
+ entries.remove(entry);
+ onRefresh();
+ }
+
+ @Override
+ public int indexOf(ListOptionEntry<?> entry) {
+ return entries.indexOf(entry);
+ }
+
+ @Override
+ public void requestSet(List<T> value) {
+ entries.clear();
+ entries.addAll(createEntries(value));
+ onRefresh();
+ listeners.forEach(listener -> listener.accept(this, value));
+ }
+
+ @Override
+ public boolean changed() {
+ return !binding().getValue().equals(pendingValue());
+ }
+
+ @Override
+ public boolean applyValue() {
+ if (changed()) {
+ binding().setValue(pendingValue());
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public void forgetPendingValue() {
+ requestSet(binding().getValue());
+ }
+
+ @Override
+ public void requestSetDefault() {
+ requestSet(binding().defaultValue());
+ }
+
+ @Override
+ public boolean isPendingValueDefault() {
+ return binding().defaultValue().equals(pendingValue());
+ }
+
+ @Override
+ public boolean available() {
+ return available;
+ }
+
+ @Override
+ public void setAvailable(boolean available) {
+ this.available = available;
+ }
+
+ @Override
+ public void addListener(BiConsumer<Option<List<T>>, List<T>> changedListener) {
+ this.listeners.add(changedListener);
+ }
+
+ @Override
+ public void addRefreshListener(Runnable changedListener) {
+ this.refreshListeners.add(changedListener);
+ }
+
+ @Override
+ public boolean isRoot() {
+ return false;
+ }
+
+ private List<ListOptionEntry<T>> createEntries(Collection<T> values) {
+ return values.stream().map(entryFactory::create).collect(Collectors.toList());
+ }
+
+ void callListeners() {
+ List<T> pendingValue = pendingValue();
+ this.listeners.forEach(listener -> listener.accept(this, pendingValue));
+ }
+
+ private void onRefresh() {
+ refreshListeners.forEach(Runnable::run);
+ callListeners();
+ }
+
+ private class EntryFactory {
+ private final Function<ListOptionEntry<T>, Controller<T>> controllerFunction;
+
+ private EntryFactory(Function<ListOptionEntry<T>, Controller<T>> controllerFunction) {
+ this.controllerFunction = controllerFunction;
+ }
+
+ public ListOptionEntry<T> create(T initialValue) {
+ return new ListOptionEntryImpl<>(ListOptionImpl.this, initialValue, controllerFunction);
+ }
+ }
+}
diff --git a/src/client/java/dev/isxander/yacl/impl/OptionGroupImpl.java b/src/client/java/dev/isxander/yacl/impl/OptionGroupImpl.java
index 58bc96b..02ef04c 100644
--- a/src/client/java/dev/isxander/yacl/impl/OptionGroupImpl.java
+++ b/src/client/java/dev/isxander/yacl/impl/OptionGroupImpl.java
@@ -6,5 +6,5 @@ import dev.isxander.yacl.api.OptionGroup;
import net.minecraft.text.Text;
import org.jetbrains.annotations.NotNull;
-public record OptionGroupImpl(@NotNull Text name, @NotNull Text tooltip, ImmutableList<Option<?>> options, boolean collapsed, boolean isRoot) implements OptionGroup {
+public record OptionGroupImpl(@NotNull Text name, @NotNull Text tooltip, ImmutableList<? extends Option<?>> options, boolean collapsed, boolean isRoot) implements OptionGroup {
}
diff --git a/src/client/java/dev/isxander/yacl/impl/OptionImpl.java b/src/client/java/dev/isxander/yacl/impl/OptionImpl.java
index 90158c7..0cc156f 100644
--- a/src/client/java/dev/isxander/yacl/impl/OptionImpl.java
+++ b/src/client/java/dev/isxander/yacl/impl/OptionImpl.java
@@ -93,11 +93,6 @@ public class OptionImpl<T> implements Option<T> {
}
@Override
- public boolean requiresRestart() {
- return flags.contains(OptionFlag.GAME_RESTART);
- }
-
- @Override
public boolean changed() {
return !binding().getValue().equals(pendingValue);
}
diff --git a/src/client/java/dev/isxander/yacl/impl/YetAnotherConfigLibImpl.java b/src/client/java/dev/isxander/yacl/impl/YetAnotherConfigLibImpl.java
index eb23eac..380929c 100644
--- a/src/client/java/dev/isxander/yacl/impl/YetAnotherConfigLibImpl.java
+++ b/src/client/java/dev/isxander/yacl/impl/YetAnotherConfigLibImpl.java
@@ -8,12 +8,77 @@ import dev.isxander.yacl.impl.utils.YACLConstants;
import net.minecraft.client.gui.screen.Screen;
import net.minecraft.text.Text;
+import java.util.Objects;
import java.util.function.Consumer;
-public record YetAnotherConfigLibImpl(Text title, ImmutableList<ConfigCategory> categories, Runnable saveFunction, Consumer<YACLScreen> initConsumer) implements YetAnotherConfigLib {
+public final class YetAnotherConfigLibImpl implements YetAnotherConfigLib {
+ private final Text title;
+ private final ImmutableList<ConfigCategory> categories;
+ private final Runnable saveFunction;
+ private final Consumer<YACLScreen> initConsumer;
+
+ private boolean generated = false;
+
+ public YetAnotherConfigLibImpl(Text title, ImmutableList<ConfigCategory> categories, Runnable saveFunction, Consumer<YACLScreen> initConsumer) {
+ this.title = title;
+ this.categories = categories;
+ this.saveFunction = saveFunction;
+ this.initConsumer = initConsumer;
+ }
+
@Override
public Screen generateScreen(Screen parent) {
+ if (generated)
+ throw new UnsupportedOperationException("To prevent memory leaks, you should only generate a Screen once per instance. Please re-build the instance to generate another GUI.");
+
YACLConstants.LOGGER.info("Generating YACL screen");
+ generated = true;
return new YACLScreen(this, parent);
}
+
+ @Override
+ public Text title() {
+ return title;
+ }
+
+ @Override
+ public ImmutableList<ConfigCategory> categories() {
+ return categories;
+ }
+
+ @Override
+ public Runnable saveFunction() {
+ return saveFunction;
+ }
+
+ @Override
+ public Consumer<YACLScreen> initConsumer() {
+ return initConsumer;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == this) return true;
+ if (obj == null || obj.getClass() != this.getClass()) return false;
+ var that = (YetAnotherConfigLibImpl) obj;
+ return Objects.equals(this.title, that.title) &&
+ Objects.equals(this.categories, that.categories) &&
+ Objects.equals(this.saveFunction, that.saveFunction) &&
+ Objects.equals(this.initConsumer, that.initConsumer);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(title, categories, saveFunction, initConsumer);
+ }
+
+ @Override
+ public String toString() {
+ return "YetAnotherConfigLibImpl[" +
+ "title=" + title + ", " +
+ "categories=" + categories + ", " +
+ "saveFunction=" + saveFunction + ", " +
+ "initConsumer=" + initConsumer + ']';
+ }
+
}
diff --git a/src/main/resources/assets/yet-another-config-lib/lang/en_us.json b/src/main/resources/assets/yet-another-config-lib/lang/en_us.json
index 17972a1..292864f 100644
--- a/src/main/resources/assets/yet-another-config-lib/lang/en_us.json
+++ b/src/main/resources/assets/yet-another-config-lib/lang/en_us.json
@@ -16,6 +16,11 @@
"yacl.gui.save_before_exit": "Save before exiting!",
"yacl.gui.save_before_exit.tooltip": "Save or cancel to exit the GUI.",
+ "yacl.list.move_up": "Move up",
+ "yacl.list.move_down": "Move down",
+ "yacl.list.remove": "Remove",
+ "yacl.list.add_top": "New entry",
+
"yacl.restart.title": "Config requires restart!",
"yacl.restart.message": "One or more options needs you to restart the game to apply the changes.",
"yacl.restart.yes": "Close Minecraft",
diff --git a/src/testmod/java/dev/isxander/yacl/test/config/ExampleConfig.java b/src/testmod/java/dev/isxander/yacl/test/config/ExampleConfig.java
index c7ea46c..5d864da 100644
--- a/src/testmod/java/dev/isxander/yacl/test/config/ExampleConfig.java
+++ b/src/testmod/java/dev/isxander/yacl/test/config/ExampleConfig.java
@@ -5,6 +5,7 @@ import dev.isxander.yacl.config.ConfigInstance;
import dev.isxander.yacl.config.GsonConfigInstance;
import java.awt.*;
+import java.util.List;
import java.nio.file.Path;
public class ExampleConfig {
@@ -25,6 +26,9 @@ public class ExampleConfig {
@ConfigEntry public long longField = 5;
@ConfigEntry public Alphabet enumOption = Alphabet.A;
+ @ConfigEntry public List<String> stringList = List.of("This is quite cool.", "You can add multiple items!", "And it is integrated so well into Option groups!");
+ @ConfigEntry public List<Integer> intList = List.of(1, 2, 3);
+
@ConfigEntry public boolean groupTestRoot = false;
@ConfigEntry public boolean groupTestFirstGroup = false;
@ConfigEntry public boolean groupTestFirstGroup2 = false;
diff --git a/src/testmod/java/dev/isxander/yacl/test/config/GuiTest.java b/src/testmod/java/dev/isxander/yacl/test/config/GuiTest.java
index 7295f2c..4965150 100644
--- a/src/testmod/java/dev/isxander/yacl/test/config/GuiTest.java
+++ b/src/testmod/java/dev/isxander/yacl/test/config/GuiTest.java
@@ -22,7 +22,8 @@ import net.minecraft.text.ClickEvent;
import net.minecraft.text.HoverEvent;
import net.minecraft.text.Text;
-import java.awt.*;
+import java.awt.Color;
+import java.util.List;
public class GuiTest {
public static Screen getModConfigScreenFactory(Screen parent) {
@@ -245,6 +246,36 @@ public class GuiTest {
.build())
.build())
.build())
+ .category(ConfigCategory.createBuilder()
+ .name(Text.of("List Test"))
+ .group(ListOption.createBuilder(String.class)
+ .name(Text.of("String List"))
+ .binding(
+ defaults.stringList,
+ () -> config.stringList,
+ val -> config.stringList = val
+ )
+ .controller(StringController::new)
+ .initial("")
+ .build())
+ .group(ListOption.createBuilder(Integer.class)
+ .name(Text.of("Slider List"))
+ .binding(
+ defaults.intList,
+ () -> config.intList,
+ val -> config.intList = val
+ )
+ .controller(opt -> new IntegerSliderController(opt, 0, 10, 1))
+ .initial(0)
+ .available(false)
+ .build())
+ .group(ListOption.createBuilder(Text.class)
+ .name(Text.of("Useless Label List"))
+ .binding(Binding.immutable(List.of(Text.of("It's quite impressive that literally every single controller works, without problem."))))
+ .controller(LabelController::new)
+ .initial(Text.of("Initial label"))
+ .build())
+ .build())
.category(PlaceholderCategory.createBuilder()
.name(Text.of("Placeholder Category"))
.screen((client, yaclScreen) -> new RequireRestartScreen(yaclScreen))