From dd65110f60aa3e32c2970863a06a7682520cce5e Mon Sep 17 00:00:00 2001 From: Xander Date: Sun, 11 Dec 2022 19:31:56 +0000 Subject: [Feature] Lists (#40) --- .../java/dev/isxander/yacl/api/ConfigCategory.java | 9 + .../java/dev/isxander/yacl/api/ListOption.java | 217 +++++++++++++++++++++ .../dev/isxander/yacl/api/ListOptionEntry.java | 23 +++ src/client/java/dev/isxander/yacl/api/Option.java | 10 +- .../java/dev/isxander/yacl/api/OptionGroup.java | 8 +- .../dev/isxander/yacl/api/utils/OptionUtils.java | 14 +- 6 files changed, 268 insertions(+), 13 deletions(-) create mode 100644 src/client/java/dev/isxander/yacl/api/ListOption.java create mode 100644 src/client/java/dev/isxander/yacl/api/ListOptionEntry.java (limited to 'src/client/java/dev/isxander/yacl/api') 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> 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 + */ +public interface ListOption extends OptionGroup, Option> { + @Override + @NotNull ImmutableList> options(); + + /** + * Class of the entry type + */ + @NotNull Class elementTypeClass(); + + @ApiStatus.Internal + ListOptionEntry 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 Builder createBuilder(Class typeClass) { + return new Builder<>(typeClass); + } + + class Builder { + private Text name = Text.empty(); + private final List tooltipLines = new ArrayList<>(); + private Function, Controller> controllerFunction; + private Binding> binding = null; + private final Set flags = new HashSet<>(); + private T initialValue; + private boolean collapsed = false; + private boolean available = true; + private final Class typeClass; + + private Builder(Class 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 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. + *

+ * 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 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 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 controller(@NotNull Function, Controller> 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 binding(@NotNull Binding> 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 binding(@NotNull List def, @NotNull Supplier<@NotNull List> getter, @NotNull Consumer<@NotNull List> 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 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 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 flags(@NotNull Collection 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 collapsed(boolean collapsible) { + this.collapsed = collapsible; + return this; + } + + public ListOption 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 extends Option { + ListOption parentGroup(); + + @Override + default @NotNull Class typeClass() { + return parentGroup().elementTypeClass(); + } + + @Override + default @NotNull ImmutableSet 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 @@ -70,12 +70,6 @@ public interface Option { */ 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. */ @@ -109,6 +103,10 @@ public interface Option { */ 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> options(); + @NotNull ImmutableList> 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> 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, 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; + } } + } } } -- cgit