diff options
4 files changed, 97 insertions, 30 deletions
diff --git a/common/src/main/java/dev/isxander/yacl3/impl/ListOptionEntryImpl.java b/common/src/main/java/dev/isxander/yacl3/impl/ListOptionEntryImpl.java index bdb8de8..72254cf 100644 --- a/common/src/main/java/dev/isxander/yacl3/impl/ListOptionEntryImpl.java +++ b/common/src/main/java/dev/isxander/yacl3/impl/ListOptionEntryImpl.java @@ -138,7 +138,7 @@ public final class ListOptionEntryImpl<T> implements ListOptionEntry<T> { @Override public void setValue(T newValue) { value = newValue; - group.callListeners(); + group.callListeners(true); } @Override diff --git a/common/src/main/java/dev/isxander/yacl3/impl/ListOptionImpl.java b/common/src/main/java/dev/isxander/yacl3/impl/ListOptionImpl.java index 12842c1..f45e368 100644 --- a/common/src/main/java/dev/isxander/yacl3/impl/ListOptionImpl.java +++ b/common/src/main/java/dev/isxander/yacl3/impl/ListOptionImpl.java @@ -4,6 +4,7 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import dev.isxander.yacl3.api.*; import dev.isxander.yacl3.api.controller.ControllerBuilder; +import dev.isxander.yacl3.impl.utils.YACLConstants; import net.minecraft.network.chat.Component; import org.apache.commons.lang3.Validate; import org.jetbrains.annotations.ApiStatus; @@ -30,8 +31,10 @@ public final class ListOptionImpl<T> implements ListOption<T> { private final boolean insertEntriesAtEnd; private final ImmutableSet<OptionFlag> flags; private final EntryFactory entryFactory; + private final List<BiConsumer<Option<List<T>>, List<T>>> listeners; private final List<Runnable> refreshListeners; + private int listenerTriggerDepth = 0; public ListOptionImpl(@NotNull Component name, @NotNull OptionDescription description, @NotNull Binding<List<T>> binding, @NotNull Supplier<T> initialValue, @NotNull Function<ListOptionEntry<T>, Controller<T>> controllerFunction, ImmutableSet<OptionFlag> flags, boolean collapsed, boolean available, int minimumNumberOfEntries, int maximumNumberOfEntries, boolean insertEntriesAtEnd, Collection<BiConsumer<Option<List<T>>, List<T>>> listeners) { this.name = name; @@ -49,7 +52,7 @@ public final class ListOptionImpl<T> implements ListOption<T> { this.listeners = new ArrayList<>(); this.listeners.addAll(listeners); this.refreshListeners = new ArrayList<>(); - callListeners(); + callListeners(true); } @Override @@ -170,7 +173,12 @@ public final class ListOptionImpl<T> implements ListOption<T> { @Override public void setAvailable(boolean available) { + boolean changed = this.available != available; + this.available = available; + + if (changed) + callListeners(false); } @Override @@ -205,14 +213,30 @@ public final class ListOptionImpl<T> implements ListOption<T> { return values.stream().map(entryFactory::create).collect(Collectors.toList()); } - void callListeners() { + void callListeners(boolean bypass) { List<T> pendingValue = pendingValue(); - this.listeners.forEach(listener -> listener.accept(this, pendingValue)); + if (bypass || listenerTriggerDepth == 0) { + if (listenerTriggerDepth > 10) { + throw new IllegalStateException("Listener trigger depth exceeded 10! This means a listener triggered a listener etc etc 10 times deep. This is likely a bug in the mod using YACL!"); + } + + this.listenerTriggerDepth++; + + for (BiConsumer<Option<List<T>>, List<T>> listener : listeners) { + try { + listener.accept(this, pendingValue); + } catch (Exception e) { + YACLConstants.LOGGER.error("Exception whilst triggering listener for option '%s'".formatted(name.getString()), e); + } + } + + this.listenerTriggerDepth--; + } } private void onRefresh() { refreshListeners.forEach(Runnable::run); - callListeners(); + callListeners(true); } private class EntryFactory { @@ -234,7 +258,7 @@ public final class ListOptionImpl<T> implements ListOption<T> { private Function<ListOptionEntry<T>, Controller<T>> controllerFunction; private Binding<List<T>> binding = null; private final Set<OptionFlag> flags = new HashSet<>(); - private T initialValue; + private Supplier<T> initialValue; private boolean collapsed = false; private boolean available = true; private int minimumNumberOfEntries = 0; @@ -259,7 +283,7 @@ public final class ListOptionImpl<T> implements ListOption<T> { } @Override - public Builder<T> initial(@NotNull T initialValue) { + public Builder<T> initial(@NotNull Supplier<T> initialValue) { Validate.notNull(initialValue, "`initialValue` cannot be empty"); this.initialValue = initialValue; @@ -267,6 +291,14 @@ public final class ListOptionImpl<T> implements ListOption<T> { } @Override + public Builder<T> initial(@NotNull T initialValue) { + Validate.notNull(initialValue, "`initialValue` cannot be empty"); + + this.initialValue = () -> initialValue; + return this; + } + + @Override public Builder<T> controller(@NotNull Function<Option<T>, ControllerBuilder<T>> controller) { Validate.notNull(controller, "`controller` cannot be null"); diff --git a/common/src/main/java/dev/isxander/yacl3/impl/OptionImpl.java b/common/src/main/java/dev/isxander/yacl3/impl/OptionImpl.java index 01a6287..165f38d 100644 --- a/common/src/main/java/dev/isxander/yacl3/impl/OptionImpl.java +++ b/common/src/main/java/dev/isxander/yacl3/impl/OptionImpl.java @@ -3,6 +3,7 @@ package dev.isxander.yacl3.impl; import com.google.common.collect.ImmutableSet; import dev.isxander.yacl3.api.*; import dev.isxander.yacl3.api.controller.ControllerBuilder; +import dev.isxander.yacl3.impl.utils.YACLConstants; import net.minecraft.ChatFormatting; import net.minecraft.network.chat.Component; import org.apache.commons.lang3.Validate; @@ -28,6 +29,7 @@ public final class OptionImpl<T> implements Option<T> { private T pendingValue; private final List<BiConsumer<Option<T>, T>> listeners; + private int listenerTriggerDepth = 0; public OptionImpl( @NotNull Component name, @@ -82,7 +84,12 @@ public final class OptionImpl<T> implements Option<T> { @Override public void setAvailable(boolean available) { + boolean changed = this.available != available; + this.available = available; + + if (changed) + this.triggerListeners(false); } @Override @@ -103,7 +110,7 @@ public final class OptionImpl<T> implements Option<T> { @Override public void requestSet(T value) { pendingValue = value; - listeners.forEach(listener -> listener.accept(this, pendingValue)); + this.triggerListeners(true); } @Override @@ -135,6 +142,26 @@ public final class OptionImpl<T> implements Option<T> { this.listeners.add(changedListener); } + private void triggerListeners(boolean bypass) { + if (bypass || listenerTriggerDepth == 0) { + if (listenerTriggerDepth > 10) { + throw new IllegalStateException("Listener trigger depth exceeded 10! This means a listener triggered a listener etc etc 10 times deep. This is likely a bug in the mod using YACL!"); + } + + this.listenerTriggerDepth++; + + for (BiConsumer<Option<T>, T> listener : listeners) { + try { + listener.accept(this, pendingValue); + } catch (Exception e) { + YACLConstants.LOGGER.error("Exception whilst triggering listener for option '%s'".formatted(name.getString()), e); + } + } + + this.listenerTriggerDepth--; + } + } + @ApiStatus.Internal public static class BuilderImpl<T> implements Builder<T> { private Component name = Component.literal("Name not specified!").withStyle(ChatFormatting.RED); diff --git a/test-common/src/main/java/dev/isxander/yacl3/test/GuiTest.java b/test-common/src/main/java/dev/isxander/yacl3/test/GuiTest.java index 634c2ce..c490799 100644 --- a/test-common/src/main/java/dev/isxander/yacl3/test/GuiTest.java +++ b/test-common/src/main/java/dev/isxander/yacl3/test/GuiTest.java @@ -17,6 +17,7 @@ import dev.isxander.yacl3.gui.controllers.string.number.DoubleFieldController; import dev.isxander.yacl3.gui.controllers.string.number.FloatFieldController; import dev.isxander.yacl3.gui.controllers.string.number.IntegerFieldController; import dev.isxander.yacl3.gui.controllers.string.number.LongFieldController; +import net.minecraft.Util; import net.minecraft.client.GraphicsStatus; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.components.toasts.SystemToast; @@ -29,6 +30,7 @@ import net.minecraft.resources.ResourceLocation; import java.awt.Color; import java.nio.file.Path; import java.util.List; +import java.util.concurrent.atomic.AtomicReference; public class GuiTest { public static Screen getModConfigScreenFactory(Screen parent) { @@ -53,6 +55,8 @@ public class GuiTest { } private static Screen getFullTestSuite(Screen parent) { + AtomicReference<Option<Boolean>> booleanOption = new AtomicReference<>(); + return YetAnotherConfigLib.create(ConfigTest.GSON, (defaults, config, builder) -> builder .title(Component.literal("Test GUI")) .category(ConfigCategory.createBuilder() @@ -60,26 +64,30 @@ public class GuiTest { .tooltip(Component.literal("Example Category Description")) .group(OptionGroup.createBuilder() .name(Component.literal("Boolean Controllers")) - .option(Option.<Boolean>createBuilder() - .name(Component.literal("Boolean Toggle")) - .description(OptionDescription.createBuilder() - .text(Component.empty() - .append(Component.literal("a").withStyle(style -> style.withHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, Component.literal("a"))))) - .append(Component.literal("b").withStyle(style -> style.withHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, Component.literal("b"))))) - .append(Component.literal("c c c c c c c c c c c c c c c c c c c c c c c c c c c c c c c c c c c c c c").withStyle(style -> style.withHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, Component.literal("c"))))) - .append(Component.literal("e").withStyle(style -> style.withHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, Component.literal("e"))))) - .withStyle(style -> style.withClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, "https://isxander.dev"))) - ) - .webpImage(Path.of("D:\\Xander\\Code\\isXander\\Controlify\\src\\main\\resources\\assets\\controlify\\textures\\screenshots\\reach-around-placement.webp"), new ResourceLocation("yacl", "e.webp")) - .build()) - .binding( - defaults.booleanToggle, - () -> config.booleanToggle, - (value) -> config.booleanToggle = value - ) - .controller(BooleanControllerBuilder::create) - .flag(OptionFlag.GAME_RESTART) - .build()) + .option(Util.make(() -> { + var opt = Option.<Boolean>createBuilder() + .name(Component.literal("Boolean Toggle")) + .description(OptionDescription.createBuilder() + .text(Component.empty() + .append(Component.literal("a").withStyle(style -> style.withHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, Component.literal("a"))))) + .append(Component.literal("b").withStyle(style -> style.withHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, Component.literal("b"))))) + .append(Component.literal("c c c c c c c c c c c c c c c c c c c c c c c c c c c c c c c c c c c c c c").withStyle(style -> style.withHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, Component.literal("c"))))) + .append(Component.literal("e").withStyle(style -> style.withHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, Component.literal("e"))))) + .withStyle(style -> style.withClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, "https://isxander.dev"))) + ) + .webpImage(Path.of("D:\\Xander\\Code\\isXander\\Controlify\\src\\main\\resources\\assets\\controlify\\textures\\screenshots\\reach-around-placement.webp"), new ResourceLocation("yacl", "e.webp")) + .build()) + .binding( + defaults.booleanToggle, + () -> config.booleanToggle, + (value) -> config.booleanToggle = value + ) + .controller(BooleanControllerBuilder::create) + .flag(OptionFlag.GAME_RESTART) + .build(); + booleanOption.set(opt); + return opt; + })) .option(Option.<Boolean>createBuilder() .name(Component.literal("Custom Boolean Toggle")) .description(val -> OptionDescription.createBuilder() @@ -94,9 +102,9 @@ public class GuiTest { .controller(opt -> BooleanControllerBuilder.create(opt) .valueFormatter(state -> state ? Component.literal("Amazing") : Component.literal("Not Amazing")) .coloured(true)) - .available(false) + .listener((opt, val) -> booleanOption.get().setAvailable(val)) .build()) - .option(Option.createBuilder(boolean.class) + .option(Option.<Boolean>createBuilder() .name(Component.literal("Tick Box")) .description(OptionDescription.of(Component.literal("There are even alternate methods of displaying the same data type!"))) .binding( |