Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Autoconfig Requirement API discussion #217

Draft
wants to merge 3 commits into
base: v8
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,11 @@

package me.shedaniel.autoconfig.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import me.shedaniel.autoconfig.requirements.DefaultRequirements;
import me.shedaniel.clothconfig2.api.DisableableWidget;
import me.shedaniel.clothconfig2.api.HideableWidget;

import java.lang.annotation.*;

public class ConfigEntry {

Expand Down Expand Up @@ -154,4 +155,167 @@ enum EnumDisplayOption {
}
}
}

public static class Requirements {
private Requirements() {}

/**
* Defines a requirement that will control whether this Config Entry GUI is enabled or disabled.
*
* <p>
* The requirement either references a handler method or another Config Entry GUI.
* </p>
*
* <p>
* If a handler method is referenced, it will be passed {@link #refArgs()} and {@link #staticArgs()}.
* </p>
*
* <p>
* If a Config Entry is referenced, its value will be compared against {@link #conditions()}.
* </p>
*
* <p>
* If a Config Entry is referenced and {@link #conditions()} is empty, an exception will be thrown.
* However if the referenced Config Entry has a <strong>boolean value</strong>, a default condition
* of {@code "true"} will be assumed.
* </p>
*
* @see DisableableWidget
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@Repeatable(EnableIfGroup.class)
public @interface EnableIf {

/**
* A {@link Ref reference} to a Handler method or a Config Entry.
*
* @see DefaultRequirements
*/
Ref value();

/**
* One or more conditions to be compared with the depended-on Config Entry's value.
* Will be parsed in the same way as {@link #staticArgs()}.
*/
String[] conditions() default {};

/**
* Zero or more {@link Ref references} to Config Entries, whose value should be passed to the handler method.
*/
Ref[] refArgs() default {};

/**
* Zero or more static values to be passed to the handler method.
*
* <p>The following parameter types are supported:
* <ul>
* <li>{@link String}: The arg will be used as-is.
* <li>{@link Character}: The first char of the arg will be used. The arg must be exactly 1 characters long.
* <li>{@link Boolean}: The arg will be checked against the following values (case-insensitive):
* <ul>
* <li>{@code true} for: {@code "true"}, {@code "t"}, or {@code "1"}.
* <li>{@code false} for: {@code "false"}, {@code "f"}, or {@code "0"}.
* </ul>
* <li>{@link Enum}: The arg will be compared against the {@link Enum#name() name values} of the expected Enum.
* The value must be an exact match (case-sensitive).
* <li>{@code short int long float double}: The arg will be parsed using the appropriate method,
* such as {@link Integer#valueOf(String)}.
* </ul>
* <p>An exception will be thrown if a match is not found, parsing fails, or the expected type is not listed above.
*/
String[] staticArgs() default {};
}

/**
* Defines a requirement that will control whether this Config Entry GUI is displayed on the screen.
*
* <p>
* Otherwise identical to {@link EnableIf}
* </p>
*
* @see EnableIf
* @see HideableWidget
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@Repeatable(DisplayIfGroup.class)
public @interface DisplayIf {

/**
* @see EnableIf#value()
*/
Ref value();

/**
* @see EnableIf#conditions()
*/
String[] conditions() default {};

/**
* @see EnableIf#refArgs()
*/
Ref[] refArgs() default {};

/**
* @see EnableIf#staticArgs()
*/
String[] staticArgs() default {};
}


/**
* Can be applied to a handler method to declare a list of {@link Ref refs} that should be passed to the handler
* as its initial arguments.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ConstParams {
Ref[] value();
}

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface EnableIfGroup {
EnableIf[] value();
Quantifier quantifier() default Quantifier.ALL;
}

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface DisplayIfGroup {
DisplayIf[] value();
Quantifier quantifier() default Quantifier.ALL;
}
}

/**
* Defines a reference to a {@link ConfigEntry}.
*
* <p>{@code value} should be the name of the {@code ConfigEntry}'s defining field.
* <p>{@code cls} defines the class in which to look for the field.
* If {@code cls} is set to the default value ({@link None None.class}),
* then the annotated class is searched instead.
*/
@Retention(RetentionPolicy.RUNTIME)
public @interface Ref {
String value();
Class<?> cls() default None.class;
}

/**
* A quantifier representing how many things are required.
*/
public enum Quantifier {
ALL, ANY, ONE, NONE
}

/**
* Used by ConfigEntry annotations to indicate no class is set.
*/
private static class None {
private None() {}
}

private static final String FOO = "FOO";
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@
import me.shedaniel.autoconfig.ConfigData;
import me.shedaniel.autoconfig.annotation.Config;
import me.shedaniel.autoconfig.annotation.ConfigEntry;
import me.shedaniel.autoconfig.annotation.ConfigEntry.Gui.EnumHandler.EnumDisplayOption;
import me.shedaniel.autoconfig.annotation.ConfigEntry.Ref;
import me.shedaniel.autoconfig.requirements.DefaultRequirements;
import me.shedaniel.autoconfig.serializer.PartitioningSerializer;
import org.jetbrains.annotations.ApiStatus;

Expand All @@ -47,6 +50,10 @@ public class ExampleConfig extends PartitioningSerializer.GlobalData {
@ConfigEntry.Gui.TransitiveObject
public ModuleB moduleB = new ModuleB();

@ConfigEntry.Category("c")
@ConfigEntry.Gui.TransitiveObject
public ModuleC moduleC = new ModuleC();

enum ExampleEnum {
FOO,
BAR,
Expand All @@ -62,7 +69,7 @@ public static class ModuleA implements ConfigData {
public ExampleEnum anEnum = ExampleEnum.FOO;

@ConfigEntry.Gui.Tooltip(count = 2)
@ConfigEntry.Gui.EnumHandler(option = ConfigEntry.Gui.EnumHandler.EnumDisplayOption.BUTTON)
@ConfigEntry.Gui.EnumHandler(option = EnumDisplayOption.BUTTON)
public ExampleEnum anEnumWithButton = ExampleEnum.FOO;

@Comment("This tooltip was automatically applied from a Jankson @Comment")
Expand Down Expand Up @@ -98,6 +105,91 @@ public static class ModuleB implements ConfigData {
public int color = 0xFFFFFF;
}

@Config(name = "module_c")
public static class ModuleC implements ConfigData {

public static class Handlers {
@ConfigEntry.Requirements.ConstParams(
@Ref(cls = DependencySubCategory.class, value = "intSlider")
)
private static boolean intSliderIsBigOrSmall(int value) {
return value > 70 || value < -70;
}
}

@ConfigEntry.Gui.PrefixText
@ConfigEntry.Gui.CollapsibleObject(startExpanded = true)
public DependencySubCategory dependencySubCategory = new DependencySubCategory();
public static class DependencySubCategory {

@ConfigEntry.Gui.Tooltip
public boolean coolToggle = false;

public boolean lameToggle = true;

@ConfigEntry.Gui.EnumHandler(option = EnumDisplayOption.BUTTON)
public DependencyDemoEnum coolEnum = DependencyDemoEnum.OKAY;

@ConfigEntry.BoundedDiscrete(min = -100, max = 100)
public int intSlider = 50;

@ConfigEntry.Requirements.EnableIf(@Ref("coolToggle"))
public boolean dependsOnCoolToggle1 = false;

@ConfigEntry.Requirements.DisplayIf(@Ref("coolToggle"))

public boolean dependsOnCoolToggle2 = false;

@ConfigEntry.Requirements.EnableIf(
value = @Ref(cls = DefaultRequirements.class, value = DefaultRequirements.IS),
refArgs = { @Ref("coolToggle"), @Ref("lameToggle") }
)
public boolean dependsOnToggleMatch = false;

@ConfigEntry.Requirements.EnableIf(@Ref(cls = Handlers.class, value = "intSliderIsBigOrSmall"))
public boolean dependsOnIntSlider = true;

@ConfigEntry.Gui.TransitiveObject
@ConfigEntry.Requirements.EnableIf(@Ref("coolToggle"))
@ConfigEntry.Requirements.EnableIf(
value = @Ref("coolEnum"),
conditions = { "GOOD", "EXCELLENT" }
)
public DependantObject dependantObject = new DependantObject();
public static class DependantObject {
@ConfigEntry.Gui.PrefixText
public boolean toggle1 = false;

@ConfigEntry.Requirements.EnableIf(
value = @Ref(cls = DefaultRequirements.class, value = DefaultRequirements.ANY_MATCH),
refArgs = @Ref(cls = DependencySubCategory.class, value = "intSlider"),
staticArgs = { "50", "100" }
)
public boolean toggle2 = true;
}

@ConfigEntry.Gui.CollapsibleObject(startExpanded = true)
@ConfigEntry.Requirements.EnableIf(@Ref("coolToggle"))
public DependantCollapsible dependantCollapsible = new DependantCollapsible();
public static class DependantCollapsible {
public boolean toggle1 = false;
public boolean toggle2 = true;
}

@ConfigEntry.Requirements.EnableIf(
@Ref(cls = DependencySubCategory.class, value = "coolToggle")
)
public List<Integer> list = Arrays.asList(1, 2, 3);

}

@ConfigEntry.Requirements.EnableIf(
@Ref(cls = DependencySubCategory.class, value = "coolToggle")
)
public boolean dependsOnCoolToggleOutside = false;

}

@Config(name = "empty")
public static class Empty implements ConfigData {

Expand Down Expand Up @@ -133,4 +225,8 @@ public PairOfIntPairs(PairOfInts first, PairOfInts second) {
this.second = second;
}
}

enum DependencyDemoEnum {
EXCELLENT, GOOD, OKAY, BAD, HORRIBLE
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package me.shedaniel.autoconfig.requirements;

import java.util.Objects;

public class DefaultRequirements {
private DefaultRequirements() {}

/**
* The condition is met when the argument is {@code true}.
*
* @see #isTrue(Boolean)
*/
public static final String TRUE = "isTrue";

/**
* The condition is met when the argument is {@code false}.
*
* @see #isFalse(Boolean)
*/
public static final String FALSE = "isFalse";

/**
* The condition is met when the first argument is equal to the second.
*
* @see #is(Object, Object)
*/
public static final String IS = "is";

/**
* The condition is met when at least one {@code arg} is equal to another.
*
* @see #anyMatch(Object[])
*/
public static final String ANY_MATCH = "anyMatch";

private static boolean isTrue(Boolean value) {
return Boolean.TRUE.equals(value);
}

private static boolean isFalse(Boolean value) {
return Boolean.FALSE.equals(value);
}

private static <T> boolean is(T value, T condition) {
return Objects.equals(value, condition);
}

private static <T> boolean anyMatch(T ...args) {
for (int i = 0; i < args.length; i++) {
for (int j = 0; j < i; j++) {
if (Objects.equals(args[i], args[j])) {
return true;
}
}
for (int j = i+1; j < args.length; j++) {
if (Objects.equals(args[i], args[j])) {
return true;
}
}
}
return false;
}
}