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

Config entry dependencies #195

Draft
wants to merge 135 commits into
base: v8
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
135 commits
Select commit Hold shift + click to select a range
c550f9f
Initial dependency work
MattSturgeon Mar 1, 2023
c7f2e0a
Translatable warning tooltip
MattSturgeon Mar 1, 2023
539b93b
Darken when disabled & improved tooltip
MattSturgeon Mar 1, 2023
95d7457
Breaking: remove colour codes from yes/no lang text
MattSturgeon Mar 1, 2023
46e5754
Stub out a "hidden" option
MattSturgeon Mar 4, 2023
45995d6
Initial hacky hiding
MattSturgeon Mar 6, 2023
6812905
Sub-Category & List UI
MattSturgeon Mar 6, 2023
9b0a8c4
oops
MattSturgeon Mar 6, 2023
e4d5da0
Introduce a "Dependency" class
MattSturgeon Mar 11, 2023
3104296
Comments and cleanup
MattSturgeon Mar 11, 2023
ea9414f
More cleanup, Dependency constructor->generators
MattSturgeon Mar 12, 2023
ec59e44
Javadoc + cleanup
MattSturgeon Mar 12, 2023
901ebc4
Add SelectionDependency
MattSturgeon Mar 12, 2023
9cca789
Two is a plural
MattSturgeon Mar 12, 2023
5343bb2
Minor optimisations & clarifications
MattSturgeon Mar 12, 2023
1b7226f
Move demo enum for consistency
MattSturgeon Mar 12, 2023
b7ed907
Autoconfig annotation implementation
MattSturgeon Mar 12, 2023
4969451
Add @DependOnEach annotation
MattSturgeon Mar 12, 2023
ef53fed
Make Dependency an interface
MattSturgeon Mar 13, 2023
eed9e56
Replace having multiple dependencies with groups
MattSturgeon Mar 13, 2023
a740fe9
Tweak
MattSturgeon Mar 13, 2023
3fba849
Introduce DependencyManager
MattSturgeon Mar 14, 2023
2aafda3
Move shared code to a new abstract GuiRegistry
MattSturgeon Mar 14, 2023
1255a24
Update annotation javadocs
MattSturgeon Mar 14, 2023
4eac7a0
Minor cleanup
MattSturgeon Mar 14, 2023
44c7ebd
DependencyManager cleanup + javadoc
MattSturgeon Mar 17, 2023
c5b0284
Remove unnecessary changes
MattSturgeon Mar 17, 2023
8a0eddc
Simplify visibleChildren filter
MattSturgeon Mar 17, 2023
bfc87ea
Initial Autoconfig demo
MattSturgeon Mar 17, 2023
fda6d1e
Add @DependsOn annotations to transitive children
MattSturgeon Mar 17, 2023
daee3c3
TextListEntry should be visually disabled
MattSturgeon Mar 17, 2023
5dbee63
Minor cleanup
MattSturgeon Mar 17, 2023
60a0272
Better handle one->many config entries
MattSturgeon Mar 18, 2023
d4a6d10
registerAdditionalDependency should take an entry
MattSturgeon Mar 18, 2023
af7d34d
Cleanup registerAdditionalDependency
MattSturgeon Mar 18, 2023
67252a2
Remove duplicates in combineDependencies()
MattSturgeon Mar 18, 2023
eb6f26e
Move setPrefix + javadoc
MattSturgeon Mar 18, 2023
c2ee2e6
Refactor DependencyManager's registry
MattSturgeon Mar 18, 2023
b59baad
Move i18n prefix logic to getEntry()
MattSturgeon Mar 18, 2023
91b4701
Initial stubbing out of complex conditions
MattSturgeon Mar 19, 2023
b3544c8
Convert Boolean & Selection to complex
MattSturgeon Mar 19, 2023
f367b20
Use EnumListEntry in autoconfig
MattSturgeon Mar 19, 2023
b455156
Rename SelectionDependency -> EnumDependency
MattSturgeon Mar 19, 2023
a655f77
Tweak/polish tooltips
MattSturgeon Mar 19, 2023
fcece1d
Static import DependencyGroup.Condition
MattSturgeon Mar 19, 2023
a8d39a2
Explain condition flags in javadoc
MattSturgeon Mar 20, 2023
880e938
Polish up NumberCondition
MattSturgeon Mar 20, 2023
c9483ca
Store dependency annotations in a set
MattSturgeon Mar 20, 2023
bfe6197
Cleanup DependencyManager
MattSturgeon Mar 20, 2023
6dbc4ae
Functional NumberDependency
MattSturgeon Mar 21, 2023
dc2a8fd
SafeVarargs seem safe
MattSturgeon Mar 21, 2023
21a49c9
Relative i18n references
MattSturgeon Mar 21, 2023
ada0cfb
Fixes + cleanup
MattSturgeon Mar 21, 2023
c01a2f2
Use relative dependency refs in ExampleConfig
MattSturgeon Mar 21, 2023
fa27f2d
Re-implement using actual boolean button text
MattSturgeon Mar 22, 2023
f7fc913
i18n key typo
MattSturgeon Mar 22, 2023
aee338f
DependencyBuilder
MattSturgeon Mar 22, 2023
117774e
Remove cruft and add some polish
MattSturgeon Mar 22, 2023
1699017
Fix broken lang text
MattSturgeon Mar 22, 2023
a865717
Change class signatures & packages
MattSturgeon Mar 22, 2023
778d4c2
Make Condition code more generic
MattSturgeon Mar 23, 2023
5507ceb
Split single/multi condition builders
MattSturgeon Mar 25, 2023
fa87293
Reorganise class packages
MattSturgeon Mar 25, 2023
bcfbaa3
More comprehensive tooltip inversion
MattSturgeon Mar 26, 2023
85296cb
Improve group tooltips
MattSturgeon Mar 26, 2023
799c1ce
Hash/Equals improvements
MattSturgeon Mar 26, 2023
8076475
Flatten group tooltips where possible
MattSturgeon Mar 26, 2023
12901b0
tweak
MattSturgeon Mar 26, 2023
d457fdf
Make boolean conditions optional
MattSturgeon Mar 26, 2023
735a0b0
tweak combineDependencies flow
MattSturgeon Mar 26, 2023
c6b0628
Fix conditions sharing the same FlagSet instance
MattSturgeon Mar 26, 2023
4b2d9ce
Separate hide behaviour from Dependency interface
MattSturgeon Mar 27, 2023
643839b
Move condition parsing out of DependencyManager
MattSturgeon Mar 27, 2023
b68d34b
Javadoc FieldBuilder methods
MattSturgeon Mar 27, 2023
aa14f86
Parse relative i18n earlier
MattSturgeon Mar 27, 2023
797e98b
Improve names in DependencyManager
MattSturgeon Mar 27, 2023
39d1b02
javadoc +rename i18nBase param
MattSturgeon Mar 27, 2023
6893b6b
Move records into their own files
MattSturgeon Mar 27, 2023
4addf3b
Store definition children in Sets
MattSturgeon Mar 28, 2023
d399713
"Dynamic" matcher conditions
MattSturgeon Mar 28, 2023
28995b1
Flatten using LinkedList
MattSturgeon Mar 30, 2023
5293b8f
Generalise condition building
MattSturgeon Mar 30, 2023
71d08ae
Let getEntry throw a runtime exception
MattSturgeon Mar 31, 2023
7239844
lameToggle doesn't need a tooltip
MattSturgeon Mar 31, 2023
b7b7fe9
Allow disabling tooltips + improve generation
MattSturgeon Mar 31, 2023
c200159
Stream tooltip lines instead of adding to a list
MattSturgeon Mar 31, 2023
801d2ce
Cleanup DependencyBuilder.matching() api
MattSturgeon Mar 31, 2023
71be351
Fix and clean some more
MattSturgeon Mar 31, 2023
0759a59
Remove equals overrides & use more sets
MattSturgeon Mar 31, 2023
4e96d6d
Improve group requirements
MattSturgeon Mar 31, 2023
26acecc
Move builders to definition records
MattSturgeon Mar 31, 2023
72a56fd
Move i18n parsing into its own util
MattSturgeon Mar 31, 2023
4baa76d
Apply prefix if lookup fails
MattSturgeon Apr 1, 2023
e9fb765
Move annotations to their own Dependency class
MattSturgeon Apr 1, 2023
62eaaff
Remove Internal annotation from package-private
MattSturgeon Apr 1, 2023
558aaab
Definition javadoc + cleanup
MattSturgeon Apr 1, 2023
0f74b16
Allow depending on unsupported types
MattSturgeon Apr 1, 2023
a29a0e2
Implement GenericCondition
MattSturgeon Apr 1, 2023
315b8c5
Require allowGeneric being set explicitly
MattSturgeon Apr 1, 2023
3f223f3
Fix a javadoc
MattSturgeon Apr 2, 2023
2ac2d26
Improve on builder API
MattSturgeon Apr 2, 2023
b8de536
Make GenericDependencyBuilder extend MultiConditionDependencyBuilder
MattSturgeon Apr 2, 2023
4a648bf
Improve BooleanDependencyBuilder checks
MattSturgeon Apr 2, 2023
74fd183
Remove MultiConditionDependencyBuilder
MattSturgeon Apr 2, 2023
24c4781
DependencyBuilder split into Start* and Finish*
MattSturgeon Apr 2, 2023
f4676f2
Tweak NumberCondition constructors
MattSturgeon Apr 2, 2023
03e5417
Generalise DependencyDefinition.buildConditions
MattSturgeon Apr 2, 2023
6c9884c
Move ConditionFlags out of Condition interface
MattSturgeon Apr 2, 2023
47110ca
Initial ListCondition implementation
MattSturgeon Apr 2, 2023
6a0bcfe
Initial ListEntryDependency & builder
MattSturgeon Apr 2, 2023
6d7bb67
Have GenericCondition parse from (not to) string
MattSturgeon Apr 3, 2023
1ba7e53
Restructuring Conditions into interfaces
MattSturgeon Apr 3, 2023
7b93cca
Stub out a MultiCondition that uses a sub-condition
MattSturgeon Apr 3, 2023
eaf42a2
Fix javadoc link
MattSturgeon Apr 9, 2023
248e841
Move condition string parsing to definition
MattSturgeon Apr 9, 2023
1c43fcb
Move condition flags to autoconfig
MattSturgeon Apr 9, 2023
79d00a9
Apply GroupRequirement to conditions too
MattSturgeon Apr 9, 2023
78736b1
PredicateCondition interface
MattSturgeon Apr 9, 2023
11e6155
Begin reworking conditions to use builders
MattSturgeon Apr 10, 2023
59d82d0
List condition builders
MattSturgeon Apr 10, 2023
35e62f6
Make DependencyGroup less special
MattSturgeon Apr 10, 2023
6e40871
Introduce new ListConfigEntry interfaces
MattSturgeon Apr 10, 2023
3badb39
Begin moving description to builders
MattSturgeon Apr 10, 2023
76acbcc
Move tooltips to builders
MattSturgeon Apr 10, 2023
0cd835b
Finish port to "new condition"
MattSturgeon Apr 19, 2023
f600a04
Fix Requirement.inverted() implementations
MattSturgeon Apr 19, 2023
cfe05dd
tweak
MattSturgeon Apr 19, 2023
8cae0b9
Improve tooltip generation
MattSturgeon Apr 19, 2023
10d9861
Implement `Condition.fullDescription()`
MattSturgeon Apr 19, 2023
0877b00
Improve tooltips/etc
MattSturgeon Apr 19, 2023
6493946
DependencyBuilder should build a Dependency
MattSturgeon Apr 19, 2023
a1e7357
Remove overly-specific dependency implementations
MattSturgeon Apr 19, 2023
e8c936b
StartDependencyBuilder can't have state & be singleton
MattSturgeon Apr 19, 2023
11a9d82
More refactoring
MattSturgeon May 5, 2023
eec25fd
Actually functional now! 😂
MattSturgeon Jul 20, 2023
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.dependencies.ConditionFlag;
import me.shedaniel.clothconfig2.api.dependencies.requirements.ComparisonOperator;
import me.shedaniel.clothconfig2.api.dependencies.requirements.GroupRequirement;

import java.lang.annotation.*;

public class ConfigEntry {

Expand Down Expand Up @@ -74,6 +75,261 @@ private ConfigEntry() {
// double max();
// }

public static class Dependency {
private Dependency() {}

/**
* Depends on the referenced field.
* If the dependency is not met, the annotated field's config entry will be disabled.
* <br><br>
* A field can be annotated multiple times.
* All defined dependencies must be met for the config entry to be enabled.
* <br><br>
* Alternatively, {@link EnableIfGroup @EnableIfGroup} can also be used to define multiple dependencies,
* optionally using alternative group-matching conditions such as {@code any}, {@code none}, or exactly {@code one}.
*
* @see EnableIfGroup
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@Repeatable(EnableIfGroup.class)
public @interface EnableIf {
/**
* The i18n key of the field to depend on.
* <br><br>
* Can be either absolute or relative. Relative keys must start with a '{@code .}' character, multiple
* '{@code .}' characters can be used to reference parent keys. Absolute keys can either include or omit
* the part or the key referencing the root {@link Config @Config} class.
* <br><br>
* For example, the following keys are equivalent when referenced from a field with key
* <em>"{@code text.autoconfig.example.option.module.childObject.someOption}"</em>:
* <ul>
* <li>Absolute: <em>"{@code text.autoconfig.example.option.module.childObject.otherOption}"</em></li>
* <li>Absolute: <em>"{@code option.module.childObject.otherOption}"</em></li>
* <li>Relative: <em>"{@code .otherOption}"</em></li>
* </ul>
* An option in a different object <em>"{@code siblingObject}"</em> could be referenced using any of the following:
* <ul>
* <li>Absolute: <em>"{@code text.autoconfig.example.option.module.siblingObject.otherOption}"</em></li>
* <li>Absolute: <em>"{@code option.module.siblingObject.otherOption}"</em></li>
* <li>Relative: <em>"{@code ..siblingObject.otherOption}"</em> (uses multiple '{@code .}' characters)</li>
* </ul>
*/
String value();

/**
* One or more conditions to be checked against the dependency's value.
* If any condition is matched, the annotated field is enabled.
* <br><br>
*
* <h2>Parsing
* <p>The value must be parsable into the appropriate type for the dependency, for example:
* <ul>
* <li>{@code "true"} or {@code "false"} for a boolean dependency</li>
* <li>The {@code toString()} value for an Enum dependency</li>
* <li>Etc</li>
* </ul>
*
* <h2>Quantity
* <ul>
* <li>
* Boolean dependencies require zero or one conditions.
* If no conditions are defined, <em>{@code "true"}</em> is assumed.
* If multiple conditions are defined, an {@link IllegalArgumentException} will be thrown.
* </li>
* <li>
* All other dependency types require one or more conditions be defined.
* If no conditions are defined, an {@link IllegalArgumentException} will be thrown.
* </li>
* </ul>
*
* <h2>Flags
* <p>The value can optionally be prefixed with "{@link ConditionFlag flags}"
* that affect how the condition is applied.
* <p>If the value starts with '<code>{</code>' then a corresponding '<code>}</code>' must be present.
* Any characters within the <code>{</code> and <code>}</code> will be interpreted as flags.
* <p>Unrecognised characters (including whitespace) within the flags section will cause an {@link IllegalArgumentException}
* to be thrown at runtime.
* <p>If you wish for your condition to literally start with a '<code>{</code>', you can start your value
* with '<code>{}{</code>' instead.
* <br><br>For example, the condition "<code>{!}Hello, world</code>" will be met when the depended-on entry's
* value <strong>does not</strong> equal "<em>Hello, world</em>".
*
* <br><br><p><strong>Valid flags include:</strong>
* <ul>
* <li>'{@code !}' <em>not</em>: the condition will be inverted.</li>
* <li>'{@code i}' <em>insensitive</em>: a text-based condition will ignore capitalization.</li>
* </ul>
*/
String[] conditions() default {};

/**
* One or more i18n keys referencing config entries. The dependency is met if the {@link #value() depended-on}
* config entry's value matches the value of at least one config entry listed here.
*
* <p>A single dependency should not define both static conditions and reference matching conditions. I.e. if
* {@link #conditions()} is used, this parameter should not be (and vice-versa). If both are present an
* {@link IllegalArgumentException} will be thrown at runtime.
*
* <p>
* All config entries must use a type compatible with the depended-on config entry referenced in {@link #value()}.
* For example, if the depended-on config entry is a {@link me.shedaniel.clothconfig2.gui.entries.BooleanListEntry boolean config entry}
* then only other boolean config entries should be listed.
*
* <p>As with static conditions, flags can be prefixed to the reference string. Refer to the static condition
* {@link #conditions() documentation} for more detail.
*
* <p>If the dependency is numeric, a {@link ComparisonOperator comparison operator}
* can be included before the i18n key (optionally separated by whitespace, for readability).
* If present the appropriate comparison will be used instead of checking equality.
* For example, if <em>'{@code >}'</em> is included before the i18n key, the condition is true when the
* depended-on config entry's value is greater than the other config entry's value.
*/
String[] matching() default {};

/**
* Determines how many conditions must be met for the dependency to be met.
*/
GroupRequirement requirement() default GroupRequirement.ANY;

/**
* Whether a tooltip describing the dependency should be generated.
* Setting <em>'{@code tooltip}'</em> to false will also hide this dependency from tooltips
* generated by any groups it is a member of.
* <p>
* If you disable auto-generated tooltips, it is strongly recommended that you implement an alternative
* way of informing users that the annotated config entry has particular dependencies.
*/
boolean tooltip() default true;

/**
* Allow building the dependency even if no type-specific builder is implemented.
*
* @deprecated it strongly recommended that generic dependencies are only used with dynamic
* {@link #matching() matcher} conditions.
*/
@Deprecated
boolean allowGeneric() default false;
}

/**
* Defines a group of dependencies, with a "group-matching" {@link GroupRequirement} to be met.
*
* @see GroupRequirement
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface EnableIfGroup {

/**
* The dependencies to be included in the group.
*/
EnableIf[] value();

/**
* The condition for this group to be met. By defaults, require all dependencies to be met.
*
* @see GroupRequirement
*/
GroupRequirement requirement() default GroupRequirement.ALL;

/**
* Whether this group should be logically inverted. For example an inverted group with an
* {@link GroupRequirement#ALL "ALL" condition} set would be considered met if any one dependency was unmet.
*/
boolean inverted() default false;

/**
* @see EnableIf#tooltip()
*/
boolean tooltip() default true;
}

/**
* Depends on the referenced field.
* If the dependency is not met, the annotated field's config entry will be completely hidden from menus.
* <br>
* A field can be annotated multiple times.
* All defined dependencies must be met for the config entry to be enabled.
* <br>
* Alternatively, {@link ShowIfGroup @ShowIfGroup} can also be used to define multiple dependencies,
* optionally using alternative group-matching conditions such as {@code any}, {@code none}, or exactly {@code one}.
*
* @see ShowIfGroup
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@Repeatable(ShowIfGroup.class)
public @interface ShowIf {

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

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

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

/**
* @see EnableIf#requirement()
*/
GroupRequirement requirement() default GroupRequirement.ANY;

/**
@see EnableIf#tooltip()
*/
boolean tooltip() default true;

/**
* @see EnableIf#allowGeneric()
* @deprecated it strongly recommended that generic dependencies are only used with dynamic
* {@link EnableIf#matching() matcher} conditions.
*/
@Deprecated
boolean allowGeneric() default false;
}

/**
* Defines a group of dependencies, with a "group-matching" {@link GroupRequirement} to be met.
*
* @see GroupRequirement
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface ShowIfGroup {

/**
* The dependencies to be included in the group.
*/
ShowIf[] value();

/**
* The condition for this group to be met. By defaults, require all dependencies to be met.
*
* @see GroupRequirement
*/
GroupRequirement requirement() default GroupRequirement.ALL;

/**
* Whether this group should be logically inverted. For example an inverted group with an
* {@link GroupRequirement#ALL "ALL" condition} set would be considered met if any one dependency was unmet.
*/
boolean inverted() default false;

/**
@see EnableIf#tooltip()
*/
boolean tooltip() default true;
}
}

public static class Gui {
private Gui() {
}
Expand Down Expand Up @@ -153,5 +409,6 @@ enum EnumDisplayOption {
BUTTON
}
}

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package me.shedaniel.autoconfig.dependencies;

import me.shedaniel.clothconfig2.api.dependencies.conditions.Condition;

import java.util.Arrays;
import java.util.EnumSet;
import java.util.Objects;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

public enum ConditionFlag {

/**
* When set, the condition check is inverted. I.e. {@link Condition#check(Object)} should return {@code true} if
* the check evaluated to {@code false} and vice-versa.
*/
INVERTED('!'),

/**
* When set, text dependencies will ignore capitalization.
*/
IGNORE_CASE('i');

private static final String VALID_FLAGS = Arrays.stream(ConditionFlag.values())
.map(flag -> flag.symbol)
.map(Objects::toString)
.collect(Collectors.joining(" ", "\" ", " \""));

private static final Pattern VALID_SYMBOL_PATTERN = Pattern.compile(Arrays.stream(ConditionFlag.values())
.map(flag -> flag.symbol)
.map(Object::toString)
.map(Pattern::quote)
.collect(Collectors.joining("|")));

private final Character symbol;

ConditionFlag(Character symbol) {
this.symbol = symbol;
}

/**
* Parse a {@link EnumSet set} of {@link ConditionFlag flags} from a {@link String} containing valid flag characters.
* If any invalid character is found, an {@link IllegalArgumentException} will be thrown.
*
* @param flags a {@link String} exclusively containing valid {@link ConditionFlag} symbols
* @return an {@link EnumSet} containing each {@link ConditionFlag} found in {@code flags}
* @throws IllegalArgumentException if {@code flags} contains any unrecognised characters
*/
public static EnumSet<ConditionFlag> parseFlags(String flags) throws IllegalArgumentException {
// Flags are case-insensitive TODO is this dumb?
String symbols = flags.toLowerCase();

// Map the flag string to an EnumSet
EnumSet<ConditionFlag> flagSet = Arrays.stream(ConditionFlag.values())
.filter(flag -> symbols.indexOf(flag.symbol) >= 0)
.collect(Collectors.toCollection(() -> EnumSet.noneOf(ConditionFlag.class)));

// Check the entire string mapped to valid flags
if (flagSet.size() != symbols.length()) {
String invalid = VALID_SYMBOL_PATTERN.matcher(symbols).replaceAll("");
throw new IllegalArgumentException("Unexpected flags \"%s\" in \"%s\", possible flags: %s"
.formatted(invalid, symbols, VALID_FLAGS));
}

return flagSet;
}

}
Loading