diff options
author | isXander <xandersmith2008@gmail.com> | 2023-08-14 23:27:45 +0100 |
---|---|---|
committer | isXander <xandersmith2008@gmail.com> | 2023-08-14 23:27:45 +0100 |
commit | b3355266572deef1a5c3e494ad162c592383e455 (patch) | |
tree | 876510a21e27d0052cb7a1501c0295a427c9dc3c /common/src | |
parent | d37e147dbb4db44a921533b572aed3e54b5c6a42 (diff) | |
download | YetAnotherConfigLib-b3355266572deef1a5c3e494ad162c592383e455.tar.gz YetAnotherConfigLib-b3355266572deef1a5c3e494ad162c592383e455.tar.bz2 YetAnotherConfigLib-b3355266572deef1a5c3e494ad162c592383e455.zip |
More-or-less complete API for YACL auto-gen
Diffstat (limited to 'common/src')
34 files changed, 768 insertions, 112 deletions
diff --git a/common/src/main/java/dev/isxander/yacl3/config/v2/api/ConfigClassHandler.java b/common/src/main/java/dev/isxander/yacl3/config/v2/api/ConfigClassHandler.java index 22e471f..645a8e8 100644 --- a/common/src/main/java/dev/isxander/yacl3/config/v2/api/ConfigClassHandler.java +++ b/common/src/main/java/dev/isxander/yacl3/config/v2/api/ConfigClassHandler.java @@ -2,6 +2,7 @@ package dev.isxander.yacl3.config.v2.api; import dev.isxander.yacl3.api.YetAnotherConfigLib; import dev.isxander.yacl3.config.v2.impl.ConfigClassHandlerImpl; +import net.minecraft.resources.ResourceLocation; import java.util.function.Function; @@ -14,6 +15,8 @@ public interface ConfigClassHandler<T> { ConfigField<?>[] fields(); + ResourceLocation id(); + YetAnotherConfigLib generateGui(); boolean supportsAutoGen(); @@ -25,6 +28,8 @@ public interface ConfigClassHandler<T> { } interface Builder<T> { + Builder<T> id(ResourceLocation id); + Builder<T> serializer(Function<ConfigClassHandler<T>, ConfigSerializer<T>> serializerFactory); Builder<T> autoGen(boolean autoGen); diff --git a/common/src/main/java/dev/isxander/yacl3/config/v2/api/ConfigField.java b/common/src/main/java/dev/isxander/yacl3/config/v2/api/ConfigField.java index 26a309f..1cd8739 100644 --- a/common/src/main/java/dev/isxander/yacl3/config/v2/api/ConfigField.java +++ b/common/src/main/java/dev/isxander/yacl3/config/v2/api/ConfigField.java @@ -1,17 +1,17 @@ package dev.isxander.yacl3.config.v2.api; -import org.jetbrains.annotations.Nullable; +import dev.isxander.yacl3.config.v2.api.autogen.AutoGenField; import java.util.Optional; public interface ConfigField<T> { - String serialName(); + FieldAccess<T> access(); - Optional<String> comment(); + ReadOnlyFieldAccess<T> defaultAccess(); - FieldAccess<T> access(); + ConfigClassHandler<?> parent(); - @Nullable OptionFactory<T> factory(); + Optional<SerialField> serial(); - boolean supportsFactory(); + Optional<AutoGenField<T>> autoGen(); } diff --git a/common/src/main/java/dev/isxander/yacl3/config/v2/api/FieldAccess.java b/common/src/main/java/dev/isxander/yacl3/config/v2/api/FieldAccess.java index aed9801..a961172 100644 --- a/common/src/main/java/dev/isxander/yacl3/config/v2/api/FieldAccess.java +++ b/common/src/main/java/dev/isxander/yacl3/config/v2/api/FieldAccess.java @@ -1,14 +1,5 @@ package dev.isxander.yacl3.config.v2.api; -import java.lang.reflect.Type; - -public interface FieldAccess<T> { - T get(); - +public interface FieldAccess<T> extends ReadOnlyFieldAccess<T> { void set(T value); - - String name(); - - Type type(); - } diff --git a/common/src/main/java/dev/isxander/yacl3/config/v2/api/OptionFactory.java b/common/src/main/java/dev/isxander/yacl3/config/v2/api/OptionFactory.java index aabfcf0..9d53e79 100644 --- a/common/src/main/java/dev/isxander/yacl3/config/v2/api/OptionFactory.java +++ b/common/src/main/java/dev/isxander/yacl3/config/v2/api/OptionFactory.java @@ -1,9 +1,15 @@ package dev.isxander.yacl3.config.v2.api; import dev.isxander.yacl3.api.Option; +import dev.isxander.yacl3.config.v2.api.autogen.OptionStorage; +import dev.isxander.yacl3.config.v2.impl.autogen.OptionFactoryRegistry; -public interface OptionFactory<T> { - Option<T> create(ConfigField<T> field); +import java.lang.annotation.Annotation; - Class<T> type(); +public interface OptionFactory<A extends Annotation, T> { + Option<T> createOption(A annotation, ConfigField<T> field, OptionStorage storage); + + static <A extends Annotation, T> void register(Class<A> annotationClass, OptionFactory<A, T> factory) { + OptionFactoryRegistry.registerOptionFactory(annotationClass, factory); + } } diff --git a/common/src/main/java/dev/isxander/yacl3/config/v2/api/ReadOnlyFieldAccess.java b/common/src/main/java/dev/isxander/yacl3/config/v2/api/ReadOnlyFieldAccess.java new file mode 100644 index 0000000..5b71e58 --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl3/config/v2/api/ReadOnlyFieldAccess.java @@ -0,0 +1,11 @@ +package dev.isxander.yacl3.config.v2.api; + +import java.lang.reflect.Type; + +public interface ReadOnlyFieldAccess<T> { + T get(); + + String name(); + + Type type(); +} diff --git a/common/src/main/java/dev/isxander/yacl3/config/v2/api/ConfigEntry.java b/common/src/main/java/dev/isxander/yacl3/config/v2/api/SerialEntry.java index 8b95c3f..e5ba001 100644 --- a/common/src/main/java/dev/isxander/yacl3/config/v2/api/ConfigEntry.java +++ b/common/src/main/java/dev/isxander/yacl3/config/v2/api/SerialEntry.java @@ -1,7 +1,5 @@ package dev.isxander.yacl3.config.v2.api; -import dev.isxander.yacl3.config.v2.impl.DefaultOptionFactory; - import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -9,10 +7,8 @@ import java.lang.annotation.Target; @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) -public @interface ConfigEntry { - Class<? extends OptionFactory<?>> factory() default DefaultOptionFactory.class; - - String serialName() default ""; +public @interface SerialEntry { + String value() default ""; String comment() default ""; } diff --git a/common/src/main/java/dev/isxander/yacl3/config/v2/api/SerialField.java b/common/src/main/java/dev/isxander/yacl3/config/v2/api/SerialField.java new file mode 100644 index 0000000..01c00d6 --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl3/config/v2/api/SerialField.java @@ -0,0 +1,9 @@ +package dev.isxander.yacl3.config.v2.api; + +import java.util.Optional; + +public interface SerialField { + String serialName(); + + Optional<String> comment(); +} diff --git a/common/src/main/java/dev/isxander/yacl3/config/v2/api/SimpleOptionFactory.java b/common/src/main/java/dev/isxander/yacl3/config/v2/api/SimpleOptionFactory.java new file mode 100644 index 0000000..5c6894e --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl3/config/v2/api/SimpleOptionFactory.java @@ -0,0 +1,82 @@ +package dev.isxander.yacl3.config.v2.api; + +import dev.isxander.yacl3.api.Option; +import dev.isxander.yacl3.api.OptionDescription; +import dev.isxander.yacl3.api.OptionFlag; +import dev.isxander.yacl3.api.controller.ControllerBuilder; +import dev.isxander.yacl3.config.v2.api.autogen.OptionStorage; +import dev.isxander.yacl3.config.v2.impl.FieldBackedBinding; +import net.minecraft.client.Minecraft; +import net.minecraft.locale.Language; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.MutableComponent; +import net.minecraft.resources.ResourceLocation; +import org.jetbrains.annotations.Nullable; + +import java.lang.annotation.Annotation; +import java.util.Set; + +public abstract class SimpleOptionFactory<A extends Annotation, T> implements OptionFactory<A, T> { + @Override + public Option<T> createOption(A annotation, ConfigField<T> field, OptionStorage storage) { + Option<T> option = Option.<T>createBuilder() + .name(this.name(annotation, field, storage)) + .description(v -> this.description(v, annotation, field, storage).build()) + .binding(new FieldBackedBinding<>(field.access(), field.defaultAccess())) + .controller(opt -> this.createController(annotation, field, storage, opt)) + .available(this.available(annotation, field, storage)) + .flags(this.flags(annotation, field, storage)) + .build(); + + postInit(annotation, field, storage, option); + return option; + } + + protected abstract ControllerBuilder<T> createController(A annotation, ConfigField<T> field, OptionStorage storage, Option<T> option); + + protected MutableComponent name(A annotation, ConfigField<T> field, OptionStorage storage) { + return Component.translatable(this.getTranslationKey(field, null)); + } + + protected OptionDescription.Builder description(T value, A annotation, ConfigField<T> field, OptionStorage storage) { + OptionDescription.Builder builder = OptionDescription.createBuilder(); + + String key = this.getTranslationKey(field, "desc"); + if (Language.getInstance().has(key)) { + builder.text(Component.translatable(key)); + } else { + key += "."; + int i = 0; + while (Language.getInstance().has(key + i++)) { + builder.text(Component.translatable(key + i)); + } + } + + String imagePath = "textures/yacl3/" + field.parent().id().getPath() + "/" + field.access().name() + ".webp"; + imagePath = imagePath.toLowerCase().replaceAll("[^a-z0-9/._:-]", "_"); + ResourceLocation imageLocation = new ResourceLocation(field.parent().id().getNamespace(), imagePath); + if (Minecraft.getInstance().getResourceManager().getResource(imageLocation).isPresent()) { + builder.webpImage(imageLocation); + } + + return builder; + } + + protected boolean available(A annotation, ConfigField<T> field, OptionStorage storage) { + return true; + } + + protected Set<OptionFlag> flags(A annotation, ConfigField<T> field, OptionStorage storage) { + return Set.of(); + } + + protected void postInit(A annotation, ConfigField<T> field, OptionStorage storage, Option<T> option) { + + } + + protected String getTranslationKey(ConfigField<T> field, @Nullable String suffix) { + String key = "yacl3.config.%s.%s".formatted(field.parent().id().toString(), field.access().name()); + if (suffix != null) key += "." + suffix; + return key; + } +} diff --git a/common/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/AutoGen.java b/common/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/AutoGen.java new file mode 100644 index 0000000..8abcb60 --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/AutoGen.java @@ -0,0 +1,14 @@ +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; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface AutoGen { + String category(); + + String group() default ""; +} diff --git a/common/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/AutoGenField.java b/common/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/AutoGenField.java new file mode 100644 index 0000000..48db22d --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/AutoGenField.java @@ -0,0 +1,9 @@ +package dev.isxander.yacl3.config.v2.api.autogen; + +import java.util.Optional; + +public interface AutoGenField<T> { + String category(); + + Optional<String> group(); +} diff --git a/common/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/Boolean.java b/common/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/Boolean.java new file mode 100644 index 0000000..bb948ac --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/Boolean.java @@ -0,0 +1,21 @@ +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; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface Boolean { + enum Formatter { + YES_NO, + TRUE_FALSE, + ON_OFF, + CUSTOM, + } + + Formatter formatter() default Formatter.TRUE_FALSE; + + boolean colored() default false; +} diff --git a/common/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/DoubleSlider.java b/common/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/DoubleSlider.java new file mode 100644 index 0000000..47c7b00 --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/DoubleSlider.java @@ -0,0 +1,18 @@ +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; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface DoubleSlider { + double min(); + + double max(); + + double step(); + + String format() default "%.2f"; +} diff --git a/common/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/FloatSlider.java b/common/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/FloatSlider.java new file mode 100644 index 0000000..8d1d8e5 --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/FloatSlider.java @@ -0,0 +1,18 @@ +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; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface FloatSlider { + float min(); + + float max(); + + float step(); + + String format() default "%.1f"; +} diff --git a/common/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/IntSlider.java b/common/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/IntSlider.java new file mode 100644 index 0000000..05be857 --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/IntSlider.java @@ -0,0 +1,16 @@ +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; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface IntSlider { + int min(); + + int max(); + + int step(); +} diff --git a/common/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/Label.java b/common/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/Label.java new file mode 100644 index 0000000..7a8ef22 --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/Label.java @@ -0,0 +1,11 @@ +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; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface Label { +} diff --git a/common/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/MasterTickBox.java b/common/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/MasterTickBox.java new file mode 100644 index 0000000..67c311d --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/MasterTickBox.java @@ -0,0 +1,14 @@ +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; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface MasterTickBox { + String[] value(); + + boolean invert() default false; +} diff --git a/common/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/OptionStorage.java b/common/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/OptionStorage.java new file mode 100644 index 0000000..f90fc29 --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/OptionStorage.java @@ -0,0 +1,12 @@ +package dev.isxander.yacl3.config.v2.api.autogen; + +import dev.isxander.yacl3.api.Option; +import org.jetbrains.annotations.Nullable; + +import java.util.function.Consumer; + +public interface OptionStorage { + @Nullable Option<?> getOption(String fieldName); + + void scheduleOptionOperation(String fieldName, Consumer<Option<?>> optionConsumer); +} diff --git a/common/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/TickBox.java b/common/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/TickBox.java new file mode 100644 index 0000000..413a32d --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/TickBox.java @@ -0,0 +1,11 @@ +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; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface TickBox { +} diff --git a/common/src/main/java/dev/isxander/yacl3/config/v2/impl/ConfigClassHandlerImpl.java b/common/src/main/java/dev/isxander/yacl3/config/v2/impl/ConfigClassHandlerImpl.java index 62aa9b6..b9274e9 100644 --- a/common/src/main/java/dev/isxander/yacl3/config/v2/impl/ConfigClassHandlerImpl.java +++ b/common/src/main/java/dev/isxander/yacl3/config/v2/impl/ConfigClassHandlerImpl.java @@ -1,24 +1,33 @@ package dev.isxander.yacl3.config.v2.impl; -import dev.isxander.yacl3.api.YetAnotherConfigLib; +import dev.isxander.yacl3.api.*; import dev.isxander.yacl3.config.v2.api.*; +import dev.isxander.yacl3.config.v2.api.autogen.AutoGen; +import dev.isxander.yacl3.config.v2.api.autogen.OptionStorage; +import dev.isxander.yacl3.config.v2.impl.autogen.OptionFactoryRegistry; import dev.isxander.yacl3.platform.YACLPlatform; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; import org.apache.commons.lang3.Validate; import java.lang.reflect.Constructor; import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.Map; import java.util.function.Function; public class ConfigClassHandlerImpl<T> implements ConfigClassHandler<T> { private final Class<T> configClass; + private final ResourceLocation id; private final boolean supportsAutoGen; private final ConfigSerializer<T> serializer; private final ConfigField<?>[] fields; private final T instance, defaults; - public ConfigClassHandlerImpl(Class<T> configClass, Function<ConfigClassHandler<T>, ConfigSerializer<T>> serializerFactory, boolean autoGen) { + public ConfigClassHandlerImpl(Class<T> configClass, ResourceLocation id, Function<ConfigClassHandler<T>, ConfigSerializer<T>> serializerFactory, boolean autoGen) { this.configClass = configClass; + this.id = id; this.supportsAutoGen = YACLPlatform.getEnvironment().isClient() && autoGen; try { @@ -30,8 +39,9 @@ public class ConfigClassHandlerImpl<T> implements ConfigClassHandler<T> { } this.fields = Arrays.stream(configClass.getDeclaredFields()) - .filter(field -> field.isAnnotationPresent(ConfigEntry.class)) - .map(field -> new ConfigFieldImpl<>(this.supportsAutoGen(), field.getAnnotation(ConfigEntry.class), new ReflectionFieldAccess<>(field, instance))) + .peek(field -> field.setAccessible(true)) + .filter(field -> field.isAnnotationPresent(SerialEntry.class) || field.isAnnotationPresent(AutoGen.class)) + .map(field -> new ConfigFieldImpl<>(new ReflectionFieldAccess<>(field, instance), new ReflectionFieldAccess<>(field, defaults), this, field.getAnnotation(SerialEntry.class), field.getAnnotation(AutoGen.class))) .toArray(ConfigField[]::new); this.serializer = serializerFactory.apply(this); } @@ -57,6 +67,11 @@ public class ConfigClassHandlerImpl<T> implements ConfigClassHandler<T> { } @Override + public ResourceLocation id() { + return this.id; + } + + @Override public boolean supportsAutoGen() { return this.supportsAutoGen; } @@ -65,7 +80,43 @@ public class ConfigClassHandlerImpl<T> implements ConfigClassHandler<T> { public YetAnotherConfigLib generateGui() { Validate.isTrue(supportsAutoGen(), "Auto GUI generation is not supported for this config class. You either need to enable it in the builder or you are attempting to create a GUI in a dedicated server environment."); - throw new IllegalStateException(); + OptionStorageImpl storage = new OptionStorageImpl(); + Map<String, CategoryAndGroups> categories = new LinkedHashMap<>(); + for (ConfigField<?> configField : fields()) { + configField.autoGen().ifPresent(autoGen -> { + CategoryAndGroups groups = categories.computeIfAbsent( + autoGen.category(), + k -> new CategoryAndGroups( + ConfigCategory.createBuilder() + .name(Component.translatable("yacl3.config.%s.category.%s".formatted(id().toString(), k))), + new LinkedHashMap<>() + ) + ); + OptionAddable group = groups.groups().computeIfAbsent(autoGen.group().orElse(""), k -> { + if (k.isEmpty()) + return groups.category(); + return OptionGroup.createBuilder() + .name(Component.translatable("yacl3.config.%s.category.%s.group.%s".formatted(id().toString(), autoGen.category(), k))); + }); + + Option<?> option = createOption(configField, storage); + storage.putOption(configField.access().name(), option); + group.option(option); + }); + } + categories.values().forEach(CategoryAndGroups::finaliseGroups); + + YetAnotherConfigLib.Builder yaclBuilder = YetAnotherConfigLib.createBuilder() + .save(this.serializer()::serialize) + .title(Component.translatable("yacl3.config.%s.title".formatted(this.id().toString()))); + categories.values().forEach(category -> yaclBuilder.category(category.category().build())); + + return yaclBuilder.build(); + } + + private <U> Option<U> createOption(ConfigField<U> configField, OptionStorage storage) { + return OptionFactoryRegistry.createOption(((ReflectionFieldAccess<?>) configField.access()).field(), configField, storage) + .orElseThrow(() -> new IllegalStateException("Failed to create option for field %s".formatted(configField.access().name()))); } @Override @@ -75,6 +126,7 @@ public class ConfigClassHandlerImpl<T> implements ConfigClassHandler<T> { public static class BuilderImpl<T> implements Builder<T> { private final Class<T> configClass; + private ResourceLocation id; private Function<ConfigClassHandler<T>, ConfigSerializer<T>> serializerFactory; private boolean autoGen; @@ -83,6 +135,12 @@ public class ConfigClassHandlerImpl<T> implements ConfigClassHandler<T> { } @Override + public Builder<T> id(ResourceLocation id) { + this.id = id; + return this; + } + + @Override public Builder<T> serializer(Function<ConfigClassHandler<T>, ConfigSerializer<T>> serializerFactory) { this.serializerFactory = serializerFactory; return this; @@ -90,12 +148,23 @@ public class ConfigClassHandlerImpl<T> implements ConfigClassHandler<T> { @Override public Builder<T> autoGen(boolean autoGen) { - throw new IllegalArgumentException(); + this.autoGen = autoGen; + return this; } @Override public ConfigClassHandler<T> build() { - return new ConfigClassHandlerImpl<>(configClass, serializerFactory, autoGen); + return new ConfigClassHandlerImpl<>(configClass, id, serializerFactory, autoGen); + } + } + + private record CategoryAndGroups(ConfigCategory.Builder category, Map<String, OptionAddable> groups) { + private void finaliseGroups() { + groups.forEach((name, group) -> { + if (group instanceof OptionGroup.Builder groupBuilder) { + category.group(groupBuilder.build()); + } + }); } } } diff --git a/common/src/main/java/dev/isxander/yacl3/config/v2/impl/ConfigFieldImpl.java b/common/src/main/java/dev/isxander/yacl3/config/v2/impl/ConfigFieldImpl.java index 68bf4b8..3d79e7e 100644 --- a/common/src/main/java/dev/isxander/yacl3/config/v2/impl/ConfigFieldImpl.java +++ b/common/src/main/java/dev/isxander/yacl3/config/v2/impl/ConfigFieldImpl.java @@ -1,74 +1,69 @@ package dev.isxander.yacl3.config.v2.impl; -import dev.isxander.yacl3.config.v2.api.ConfigEntry; -import dev.isxander.yacl3.config.v2.api.ConfigField; -import dev.isxander.yacl3.config.v2.api.FieldAccess; -import dev.isxander.yacl3.config.v2.api.OptionFactory; -import org.apache.commons.lang3.NotImplementedException; +import dev.isxander.yacl3.config.v2.api.*; +import dev.isxander.yacl3.config.v2.api.autogen.AutoGen; +import dev.isxander.yacl3.config.v2.api.autogen.AutoGenField; import org.jetbrains.annotations.Nullable; -import java.lang.reflect.Constructor; import java.util.Optional; public class ConfigFieldImpl<T> implements ConfigField<T> { - private final @Nullable OptionFactory<T> factory; - private final String serialName; - private final Optional<String> comment; private final FieldAccess<T> field; - private final boolean autoGen; + private final ReadOnlyFieldAccess<T> defaultField; + private final ConfigClassHandler<?> parent; + private final Optional<SerialField> serial; + private final Optional<AutoGenField<T>> autoGen; - public ConfigFieldImpl(boolean auto, ConfigEntry entry, FieldAccess<T> field) { - this.serialName = "".equals(entry.serialName()) ? field.name() : entry.serialName(); - this.comment = "".equals(entry.comment()) ? Optional.empty() : Optional.of(entry.comment()); - this.factory = auto ? makeFactory(entry.factory(), this.serialName) : null; - this.autoGen = auto; + public ConfigFieldImpl(FieldAccess<T> field, ReadOnlyFieldAccess<T> defaultField, ConfigClassHandler<?> parent, @Nullable SerialEntry config, @Nullable AutoGen autoGen) { this.field = field; + this.defaultField = defaultField; + this.parent = parent; + + this.serial = config != null + ? Optional.of( + new SerialFieldImpl( + "".equals(config.value()) ? field.name() : config.value(), + "".equals(config.comment()) ? Optional.empty() : Optional.of(config.comment()) + ) + ) + : Optional.empty(); + this.autoGen = autoGen != null + ? Optional.of( + new AutoGenFieldImpl<>( + autoGen.category(), + "".equals(autoGen.group()) ? Optional.empty() : Optional.of(autoGen.group()) + ) + ) + : Optional.empty(); } @Override - public String serialName() { - return this.serialName; + public FieldAccess<T> access() { + return field; } @Override - public Optional<String> comment() { - return this.comment; + public ReadOnlyFieldAccess<T> defaultAccess() { + return defaultField; } @Override - public FieldAccess<T> access() { - return field; + public ConfigClassHandler<?> parent() { + return parent; } @Override - public @Nullable OptionFactory<T> factory() { - return factory; + public Optional<SerialField> serial() { + return this.serial; } @Override - public boolean supportsFactory() { + public Optional<AutoGenField<T>> autoGen() { return this.autoGen; } - private OptionFactory<T> makeFactory(Class<? extends OptionFactory<?>> clazz, String name) { - if (clazz.equals(DefaultOptionFactory.class)) { - throw new NotImplementedException("Field '%s' does not have an option factory, but auto-gen is enabled.".formatted(this.serialName())); - } - - Constructor<?> constructor; - - try { - constructor = clazz.getConstructor(String.class); - } catch (NoSuchMethodException e) { - throw new IllegalStateException("Failed to find (String) constructor for option factory %s.".formatted(clazz.getName()), e); - } - - try { - return (OptionFactory<T>) constructor.newInstance(name); - } catch (ClassCastException e) { - throw new IllegalStateException("Failed to cast option factory %s to OptionFactory<%s>.".formatted(clazz.getName(), field.type().getTypeName()), e); - } catch (ReflectiveOperationException e) { - throw new IllegalStateException("Failed to create new option factory (class is '%s')".formatted(clazz.getName()), e); - } + private record SerialFieldImpl(String serialName, Optional<String> comment) implements SerialField { + } + private record AutoGenFieldImpl<T>(String category, Optional<String> group) implements AutoGenField<T> { } } diff --git a/common/src/main/java/dev/isxander/yacl3/config/v2/impl/DefaultOptionFactory.java b/common/src/main/java/dev/isxander/yacl3/config/v2/impl/DefaultOptionFactory.java deleted file mode 100644 index e32de00..0000000 --- a/common/src/main/java/dev/isxander/yacl3/config/v2/impl/DefaultOptionFactory.java +++ /dev/null @@ -1,18 +0,0 @@ -package dev.isxander.yacl3.config.v2.impl; - -import dev.isxander.yacl3.api.Option; -import dev.isxander.yacl3.config.v2.api.ConfigField; -import dev.isxander.yacl3.config.v2.api.OptionFactory; -import org.apache.commons.lang3.NotImplementedException; - -public class DefaultOptionFactory implements OptionFactory<Object> { - @Override - public Option<Object> create(ConfigField<Object> field) { - throw new NotImplementedException(); - } - - @Override - public Class<Object> type() { - throw new NotImplementedException(); - } -} diff --git a/common/src/main/java/dev/isxander/yacl3/config/v2/impl/FieldBackedBinding.java b/common/src/main/java/dev/isxander/yacl3/config/v2/impl/FieldBackedBinding.java new file mode 100644 index 0000000..f2f36e7 --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl3/config/v2/impl/FieldBackedBinding.java @@ -0,0 +1,22 @@ +package dev.isxander.yacl3.config.v2.impl; + +import dev.isxander.yacl3.api.Binding; +import dev.isxander.yacl3.config.v2.api.FieldAccess; +import dev.isxander.yacl3.config.v2.api.ReadOnlyFieldAccess; + +public record FieldBackedBinding<T>(FieldAccess<T> field, ReadOnlyFieldAccess<T> defaultField) implements Binding<T> { + @Override + public T getValue() { + return field.get(); + } + + @Override + public void setValue(T value) { + field.set(value); + } + + @Override + public T defaultValue() { + return defaultField.get(); + } +} diff --git a/common/src/main/java/dev/isxander/yacl3/config/v2/impl/OptionStorageImpl.java b/common/src/main/java/dev/isxander/yacl3/config/v2/impl/OptionStorageImpl.java new file mode 100644 index 0000000..3c75a11 --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl3/config/v2/impl/OptionStorageImpl.java @@ -0,0 +1,37 @@ +package dev.isxander.yacl3.config.v2.impl; + +import dev.isxander.yacl3.api.Option; +import dev.isxander.yacl3.config.v2.api.autogen.OptionStorage; +import org.jetbrains.annotations.Nullable; + +import java.util.HashMap; +import java.util.Map; +import java.util.function.Consumer; + +public class OptionStorageImpl implements OptionStorage { + private final Map<String, Option<?>> storage = new HashMap<>(); + private final Map<String, Consumer<Option<?>>> scheduledOperations = new HashMap<>(); + + @Override + public @Nullable Option<?> getOption(String fieldName) { + return storage.get(fieldName); + } + + @Override + public void scheduleOptionOperation(String fieldName, Consumer<Option<?>> optionConsumer) { + if (storage.containsKey(fieldName)) { + optionConsumer.accept(storage.get(fieldName)); + } else { + scheduledOperations.merge(fieldName, optionConsumer, Consumer::andThen); + } + } + + public void putOption(String fieldName, Option<?> option) { + storage.put(fieldName, option); + + Consumer<Option<?>> consumer = scheduledOperations.remove(fieldName); + if (consumer != null) { + consumer.accept(option); + } + } +} diff --git a/common/src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/BooleanImpl.java b/common/src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/BooleanImpl.java new file mode 100644 index 0000000..0a24cf5 --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/BooleanImpl.java @@ -0,0 +1,25 @@ +package dev.isxander.yacl3.config.v2.impl.autogen; + +import dev.isxander.yacl3.api.Option; +import dev.isxander.yacl3.api.controller.BooleanControllerBuilder; +import dev.isxander.yacl3.api.controller.ControllerBuilder; +import dev.isxander.yacl3.config.v2.api.ConfigField; +import dev.isxander.yacl3.config.v2.api.SimpleOptionFactory; +import dev.isxander.yacl3.config.v2.api.autogen.Boolean; +import dev.isxander.yacl3.config.v2.api.autogen.OptionStorage; +import net.minecraft.network.chat.Component; + +public class BooleanImpl extends SimpleOptionFactory<Boolean, java.lang.Boolean> { + @Override + protected ControllerBuilder<java.lang.Boolean> createController(Boolean annotation, ConfigField<java.lang.Boolean> field, OptionStorage storage, Option<java.lang.Boolean> option) { + var builder = BooleanControllerBuilder.create(option) + .coloured(annotation.colored()); + switch (annotation.formatter()) { + case ON_OFF -> builder.onOffFormatter(); + case YES_NO -> builder.yesNoFormatter(); + case TRUE_FALSE -> builder.trueFalseFormatter(); + case CUSTOM -> builder.valueFormatter(v -> Component.translatable(getTranslationKey(field, java.lang.Boolean.toString(v)))); + } + return builder; + } +} diff --git a/common/src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/DoubleSliderImpl.java b/common/src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/DoubleSliderImpl.java new file mode 100644 index 0000000..87db158 --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/DoubleSliderImpl.java @@ -0,0 +1,30 @@ +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.DoubleSliderControllerBuilder; +import dev.isxander.yacl3.config.v2.api.ConfigField; +import dev.isxander.yacl3.config.v2.api.SimpleOptionFactory; +import dev.isxander.yacl3.config.v2.api.autogen.DoubleSlider; +import dev.isxander.yacl3.config.v2.api.autogen.OptionStorage; +import net.minecraft.locale.Language; +import net.minecraft.network.chat.Component; + +public class DoubleSliderImpl extends SimpleOptionFactory<DoubleSlider, Double> { + @Override + protected ControllerBuilder<Double> createController(DoubleSlider annotation, ConfigField<Double> field, OptionStorage storage, Option<Double> option) { + return DoubleSliderControllerBuilder.create(option) + .valueFormatter(v -> { + String key = null; + if (v == annotation.min()) + key = getTranslationKey(field, "fmt.min"); + else if (v == annotation.max()) + key = getTranslationKey(field, "fmt.max"); + if (key != null && Language.getInstance().has(key)) + return Component.translatable(key); + return Component.translatable(String.format(annotation.format(), v)); + }) + .range(annotation.min(), annotation.max()) + .step(annotation.step()); + } +} diff --git a/common/src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/FloatSliderImpl.java b/common/src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/FloatSliderImpl.java new file mode 100644 index 0000000..b856a7a --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/FloatSliderImpl.java @@ -0,0 +1,30 @@ +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.FloatSliderControllerBuilder; +import dev.isxander.yacl3.config.v2.api.ConfigField; +import dev.isxander.yacl3.config.v2.api.SimpleOptionFactory; +import dev.isxander.yacl3.config.v2.api.autogen.FloatSlider; +import dev.isxander.yacl3.config.v2.api.autogen.OptionStorage; +import net.minecraft.locale.Language; +import net.minecraft.network.chat.Component; + +public class FloatSliderImpl extends SimpleOptionFactory<FloatSlider, Float> { + @Override + protected ControllerBuilder<Float> createController(FloatSlider annotation, ConfigField<Float> field, OptionStorage storage, Option<Float> option) { + return FloatSliderControllerBuilder.create(option) + .valueFormatter(v -> { + String key = null; + if (v == annotation.min()) + key = getTranslationKey(field, "fmt.min"); + else if (v == annotation.max()) + key = getTranslationKey(field, "fmt.max"); + if (key != null && Language.getInstance().has(key)) + return Component.translatable(key); + return Component.translatable(String.format(annotation.format(), v)); + }) + .range(annotation.min(), annotation.max()) + .step(annotation.step()); + } +} diff --git a/common/src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/IntSliderImpl.java b/common/src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/IntSliderImpl.java new file mode 100644 index 0000000..c03d370 --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/IntSliderImpl.java @@ -0,0 +1,26 @@ +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.IntegerSliderControllerBuilder; +import dev.isxander.yacl3.config.v2.api.ConfigField; +import dev.isxander.yacl3.config.v2.api.SimpleOptionFactory; +import dev.isxander.yacl3.config.v2.api.autogen.IntSlider; +import dev.isxander.yacl3.config.v2.api.autogen.OptionStorage; +import net.minecraft.locale.Language; +import net.minecraft.network.chat.Component; + +public class IntSliderImpl extends SimpleOptionFactory<IntSlider, Integer> { + @Override + protected ControllerBuilder<Integer> createController(IntSlider annotation, ConfigField<Integer> field, OptionStorage storage, Option<Integer> option) { + return IntegerSliderControllerBuilder.create(option) + .valueFormatter(v -> { + String key = getTranslationKey(field, "fmt." + v); + if (Language.getInstance().has(key)) + return Component.translatable(key); + return Component.literal(Integer.toString(v)); + }) + .range(annotation.min(), annotation.max()) + .step(annotation.step()); + } +} diff --git a/common/src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/LabelImpl.java b/common/src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/LabelImpl.java new file mode 100644 index 0000000..c36c8b7 --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/LabelImpl.java @@ -0,0 +1,16 @@ +package dev.isxander.yacl3.config.v2.impl.autogen; + +import dev.isxander.yacl3.api.LabelOption; +import dev.isxander.yacl3.api.Option; +import dev.isxander.yacl3.config.v2.api.ConfigField; +import dev.isxander.yacl3.config.v2.api.OptionFactory; +import dev.isxander.yacl3.config.v2.api.autogen.Label; +import dev.isxander.yacl3.config.v2.api.autogen.OptionStorage; +import net.minecraft.network.chat.Component; + +public class LabelImpl implements OptionFactory<Label, Component> { + @Override + public Option<Component> createOption(Label annotation, ConfigField<Component> field, OptionStorage storage) { + return LabelOption.create(field.access().get()); + } +} diff --git a/common/src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/MasterTickBoxImpl.java b/common/src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/MasterTickBoxImpl.java new file mode 100644 index 0000000..65433a3 --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/MasterTickBoxImpl.java @@ -0,0 +1,27 @@ +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.TickBoxControllerBuilder; +import dev.isxander.yacl3.config.v2.api.ConfigField; +import dev.isxander.yacl3.config.v2.api.SimpleOptionFactory; +import dev.isxander.yacl3.config.v2.api.autogen.MasterTickBox; +import dev.isxander.yacl3.config.v2.api.autogen.OptionStorage; + +public class MasterTickBoxImpl extends SimpleOptionFactory<MasterTickBox, Boolean> { + @Override + protected ControllerBuilder<Boolean> createController(MasterTickBox annotation, ConfigField<Boolean> field, OptionStorage storage, Option<Boolean> option) { + return TickBoxControllerBuilder.create(option); + } + + @Override + protected void postInit(MasterTickBox annotation, ConfigField<Boolean> field, OptionStorage storage, Option<Boolean> option) { + option.addListener((opt, val) -> { + for (String child : annotation.value()) { + storage.scheduleOptionOperation(child, childOpt -> { + childOpt.setAvailable(annotation.invert() != val); + }); + } + }); + } +} 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 new file mode 100644 index 0000000..1762f2d --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/OptionFactoryRegistry.java @@ -0,0 +1,54 @@ +package dev.isxander.yacl3.config.v2.impl.autogen; + +import dev.isxander.yacl3.api.Option; +import dev.isxander.yacl3.config.v2.api.ConfigField; +import dev.isxander.yacl3.config.v2.api.OptionFactory; +import dev.isxander.yacl3.config.v2.api.autogen.*; +import dev.isxander.yacl3.config.v2.api.autogen.Boolean; +import dev.isxander.yacl3.config.v2.impl.autogen.*; +import dev.isxander.yacl3.impl.utils.YACLConstants; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Field; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +public class OptionFactoryRegistry { + private static final Map<Class<?>, OptionFactory<?, ?>> factoryMap = new HashMap<>(); + + static { + registerOptionFactory(TickBox.class, new TickBoxImpl()); + registerOptionFactory(Boolean.class, new BooleanImpl()); + registerOptionFactory(IntSlider.class, new IntSliderImpl()); + registerOptionFactory(FloatSlider.class, new FloatSliderImpl()); + registerOptionFactory(DoubleSlider.class, new DoubleSliderImpl()); + registerOptionFactory(Label.class, new LabelImpl()); + + registerOptionFactory(MasterTickBox.class, new MasterTickBoxImpl()); + } + + public static <A extends Annotation, T> void registerOptionFactory(Class<A> annotation, OptionFactory<A, T> factory) { + factoryMap.put(annotation, factory); + } + + public static <T> Optional<Option<T>> createOption(Field field, ConfigField<T> configField, OptionStorage storage) { + Annotation[] annotations = Arrays.stream(field.getAnnotations()) + .filter(annotation -> factoryMap.containsKey(annotation.annotationType())) + .toArray(Annotation[]::new); + + if (annotations.length != 1) { + YACLConstants.LOGGER.warn("Found {} option factory annotations on field {}, expected 1", annotations.length, field); + + if (annotations.length == 0) { + return Optional.empty(); + } + } + + Annotation annotation = annotations[0]; + // noinspection unchecked + OptionFactory<Annotation, T> factory = (OptionFactory<Annotation, T>) factoryMap.get(annotation.annotationType()); + return Optional.of(factory.createOption(annotation, configField, storage)); + } +} diff --git a/common/src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/TickBoxImpl.java b/common/src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/TickBoxImpl.java new file mode 100644 index 0000000..202534e --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/TickBoxImpl.java @@ -0,0 +1,16 @@ +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.TickBoxControllerBuilder; +import dev.isxander.yacl3.config.v2.api.ConfigField; +import dev.isxander.yacl3.config.v2.api.SimpleOptionFactory; +import dev.isxander.yacl3.config.v2.api.autogen.OptionStorage; +import dev.isxander.yacl3.config.v2.api.autogen.TickBox; + +public class TickBoxImpl extends SimpleOptionFactory<TickBox, Boolean> { + @Override + protected ControllerBuilder<Boolean> createController(TickBox annotation, ConfigField<Boolean> field, OptionStorage storage, Option<Boolean> option) { + return TickBoxControllerBuilder.create(option); + } +} 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 8bbc079..712a459 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,10 +1,7 @@ package dev.isxander.yacl3.config.v2.impl.serializer; import com.google.gson.*; -import dev.isxander.yacl3.config.v2.api.ConfigClassHandler; -import dev.isxander.yacl3.config.v2.api.ConfigField; -import dev.isxander.yacl3.config.v2.api.ConfigSerializer; -import dev.isxander.yacl3.config.v2.api.GsonConfigSerializerBuilder; +import dev.isxander.yacl3.config.v2.api.*; import dev.isxander.yacl3.impl.utils.YACLConstants; import dev.isxander.yacl3.platform.YACLPlatform; import net.minecraft.network.chat.Component; @@ -35,14 +32,19 @@ public class GsonConfigSerializer<T> extends ConfigSerializer<T> { JsonObject root = new JsonObject(); for (ConfigField<?> field : config.fields()) { - if (YACLPlatform.isDevelopmentEnv() && field.comment().isPresent()) { - YACLConstants.LOGGER.error("Config field '{}' has a comment, but comments are not supported by Gson. Please remove the comment or switch to a different serializer. This log will not be shown in production.", field.serialName()); + SerialField serial = field.serial().orElse(null); + if (serial == null) { + continue; + } + + if (YACLPlatform.isDevelopmentEnv() && serial.comment().isPresent()) { + YACLConstants.LOGGER.error("Config field '{}' has a comment, but comments are not supported by Gson. Please remove the comment or switch to a different serializer. This log will not be shown in production.", serial.serialName()); } try { - root.add(field.serialName(), gson.toJsonTree(field.access().get())); + root.add(serial.serialName(), gson.toJsonTree(field.access().get())); } catch (Exception e) { - YACLConstants.LOGGER.error("Failed to serialize config field '{}'.", field.serialName(), e); + YACLConstants.LOGGER.error("Failed to serialize config field '{}'.", serial.serialName(), e); } } @@ -75,17 +77,22 @@ public class GsonConfigSerializer<T> extends ConfigSerializer<T> { List<String> unconsumedKeys = new ArrayList<>(root.keySet()); for (ConfigField<?> field : config.fields()) { - if (root.containsKey(field.serialName())) { + SerialField serial = field.serial().orElse(null); + if (serial == null) { + continue; + } + + if (root.containsKey(serial.serialName())) { try { - field.access().set(gson.fromJson(root.get(field.serialName()), field.access().type())); + field.access().set(gson.fromJson(root.get(serial.serialName()), field.access().type())); } catch (Exception e) { - YACLConstants.LOGGER.error("Failed to deserialize config field '{}'.", field.serialName(), e); + YACLConstants.LOGGER.error("Failed to deserialize config field '{}'.", serial.serialName(), e); } } else { - YACLConstants.LOGGER.warn("Config field '{}' was not found in the config file. Skipping.", field.serialName()); + YACLConstants.LOGGER.warn("Config field '{}' was not found in the config file. Skipping.", serial.serialName()); } - unconsumedKeys.remove(field.serialName()); + unconsumedKeys.remove(serial.serialName()); } if (!unconsumedKeys.isEmpty()) { diff --git a/common/src/main/java/dev/isxander/yacl3/gui/controllers/string/StringControllerElement.java b/common/src/main/java/dev/isxander/yacl3/gui/controllers/string/StringControllerElement.java index 86cc7bd..383e188 100644 --- a/common/src/main/java/dev/isxander/yacl3/gui/controllers/string/StringControllerElement.java +++ b/common/src/main/java/dev/isxander/yacl3/gui/controllers/string/StringControllerElement.java @@ -5,6 +5,7 @@ import dev.isxander.yacl3.api.utils.Dimension; import dev.isxander.yacl3.gui.YACLScreen; import dev.isxander.yacl3.gui.controllers.ControllerWidget; import dev.isxander.yacl3.gui.utils.GuiUtils; +import dev.isxander.yacl3.gui.utils.UndoRedoHelper; import net.minecraft.ChatFormatting; import net.minecraft.client.gui.GuiGraphics; import net.minecraft.client.gui.screens.Screen; @@ -21,9 +22,10 @@ public class StringControllerElement extends ControllerWidget<IStringController< protected int caretPos; protected int selectionLength; - protected int renderOffset; + protected UndoRedoHelper undoRedoHelper; + protected float ticks; private final Component emptyText; @@ -35,7 +37,9 @@ public class StringControllerElement extends ControllerWidget<IStringController< inputFieldFocused = false; selectionLength = 0; emptyText = Component.literal("Click to type...").withStyle(ChatFormatting.GRAY); - control.option().addListener((opt, val) -> inputField = control.getString()); + control.option().addListener((opt, val) -> { + inputField = control.getString(); + }); setDimension(dim); } @@ -109,6 +113,10 @@ public class StringControllerElement extends ControllerWidget<IStringController< selectionLength = 0; } +// if (undoRedoHelper == null) { +// undoRedoHelper = new UndoRedoHelper(inputField, caretPos, selectionLength); +// } + return true; } else { inputFieldFocused = false; @@ -187,12 +195,26 @@ public class StringControllerElement extends ControllerWidget<IStringController< doDelete(); return true; } +// case InputConstants.KEY_Z -> { +// if (Screen.hasControlDown()) { +// UndoRedoHelper.FieldState updated = Screen.hasShiftDown() ? undoRedoHelper.redo() : undoRedoHelper.undo(); +// if (updated != null) { +// System.out.println("Updated: " + updated); +// if (modifyInput(builder -> builder.replace(0, inputField.length(), updated.text()))) { +// caretPos = updated.cursorPos(); +// selectionLength = updated.selectionLength(); +// checkRenderOffset(); +// } +// } +// return true; +// } +// } } if (Screen.isPaste(keyCode)) { return doPaste(); } else if (Screen.isCopy(keyCode)) { - return doCopy(); + return doCopy(); } else if (Screen.isCut(keyCode)) { return doCut(); } else if (Screen.isSelectAll(keyCode)) { @@ -204,6 +226,7 @@ public class StringControllerElement extends ControllerWidget<IStringController< protected boolean doPaste() { this.write(client.keyboardHandler.getClipboard()); + updateUndoHistory(); return true; } @@ -219,6 +242,7 @@ public class StringControllerElement extends ControllerWidget<IStringController< if (selectionLength != 0) { client.keyboardHandler.setClipboard(getSelection()); this.write(""); + updateUndoHistory(); return true; } return false; @@ -255,9 +279,13 @@ public class StringControllerElement extends ControllerWidget<IStringController< if (!inputFieldFocused) return false; - write(Character.toString(chr)); + if (!Screen.hasControlDown()) { + write(Character.toString(chr)); + updateUndoHistory(); + return true; + } - return true; + return false; } protected void doBackspace() { @@ -269,6 +297,7 @@ public class StringControllerElement extends ControllerWidget<IStringController< checkRenderOffset(); } } + updateUndoHistory(); } protected void doDelete() { @@ -277,6 +306,7 @@ public class StringControllerElement extends ControllerWidget<IStringController< } else if (caretPos < inputField.length()) { modifyInput(builder -> builder.deleteCharAt(caretPos)); } + updateUndoHistory(); } public void write(String string) { @@ -308,6 +338,10 @@ public class StringControllerElement extends ControllerWidget<IStringController< return true; } + protected void updateUndoHistory() { +// undoRedoHelper.save(inputField, caretPos, selectionLength); + } + public int getUnshiftedLength() { if (optionNameString.isEmpty()) return getDimension().width() - getXPadding() * 2; diff --git a/common/src/main/java/dev/isxander/yacl3/gui/utils/UndoRedoHelper.java b/common/src/main/java/dev/isxander/yacl3/gui/utils/UndoRedoHelper.java new file mode 100644 index 0000000..3328c16 --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl3/gui/utils/UndoRedoHelper.java @@ -0,0 +1,42 @@ +package dev.isxander.yacl3.gui.utils; + +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; + +public class UndoRedoHelper { + private final List<FieldState> history = new ArrayList<>(); + private int index = 0; + + public UndoRedoHelper(String text, int cursorPos, int selectionLength) { + history.add(new FieldState(text, cursorPos, selectionLength)); + } + + public void save(String text, int cursorPos, int selectionLength) { + int max = history.size(); + history.subList(index, max).clear(); + history.add(new FieldState(text, cursorPos, selectionLength)); + index++; + } + + public @Nullable FieldState undo() { + index--; + index = Math.max(index, 0); + + if (history.isEmpty()) + return null; + return history.get(index); + } + + public @Nullable FieldState redo() { + if (index < history.size() - 1) { + index++; + return history.get(index); + } else { + return null; + } + } + + public record FieldState(String text, int cursorPos, int selectionLength) {} +} |