Files
Mindustry/core/src/mindustry/editor/MapObjectivesDialog.java
Redstonneur1256 8626082068 Allow markers to be used as light sources (#9733)
* Allow markers to be used as light sources

* Better light source markers

* Move PointMarker drawLight to LightMarker

* Add a rule for unit lights

* Optimize marker rendering

* Update core/assets/bundles/bundle.properties

---------

Co-authored-by: Anuken <arnukren@gmail.com>
2025-02-09 12:10:45 -05:00

762 lines
32 KiB
Java

package mindustry.editor;
import arc.*;
import arc.func.*;
import arc.graphics.*;
import arc.math.geom.*;
import arc.scene.event.*;
import arc.scene.ui.*;
import arc.scene.ui.TextField.*;
import arc.scene.ui.layout.*;
import arc.struct.*;
import arc.util.*;
import mindustry.content.*;
import mindustry.ctype.*;
import mindustry.game.*;
import mindustry.game.MapObjectives.*;
import mindustry.gen.*;
import mindustry.graphics.*;
import mindustry.io.*;
import mindustry.type.*;
import mindustry.ui.*;
import mindustry.ui.dialogs.*;
import mindustry.world.*;
import java.lang.annotation.*;
import java.lang.reflect.*;
import static mindustry.Vars.*;
import static mindustry.editor.MapObjectivesCanvas.*;
@SuppressWarnings({"unchecked", "rawtypes"})
public class MapObjectivesDialog extends BaseDialog{
public MapObjectivesCanvas canvas;
protected Cons<Seq<MapObjective>> out = arr -> {};
/** Defines default value providers. */
private static final ObjectMap<Class<?>, FieldProvider<?>> providers = new ObjectMap<>();
/** Maps annotation type with its field parsers. Non-annotated fields are mapped with {@link Override}. */
private static final ObjectMap<Class<? extends Annotation>, ObjectMap<Class<?>, FieldInterpreter<?>>> interpreters = new ObjectMap<>();
static{
// Default un-annotated field interpreters.
setProvider(String.class, (type, cons) -> cons.get(""));
setInterpreter(String.class, (cont, name, type, field, remover, indexer, get, set) -> {
name(cont, name, remover, indexer);
if(field != null && field.isAnnotationPresent(Multiline.class)){
cont.area(get.get(), set).height(100f).growX();
}else{
cont.field(get.get(), set).growX();
}
});
setProvider(boolean.class, (type, cons) -> cons.get(false));
setInterpreter(boolean.class, (cont, name, type, field, remover, indexer, get, set) -> {
name(cont, name, remover, indexer);
cont.check("", get.get(), set::get).growX().fillY().get().getLabelCell().growX();
});
setProvider(byte.class, (type, cons) -> cons.get((byte)0));
setInterpreter(byte.class, (cont, name, type, field, remover, indexer, get, set) -> {
name(cont, name, remover, indexer);
cont.field(Byte.toString(get.get()), str -> set.get((byte)Strings.parseInt(str)))
.growX().fillY()
.valid(Strings::canParseInt)
.get().setFilter(TextFieldFilter.digitsOnly);
});
setProvider(int.class, (type, cons) -> cons.get(0));
setInterpreter(int.class, (cont, name, type, field, remover, indexer, get, set) -> {
name(cont, name, remover, indexer);
cont.field(Integer.toString(get.get()), str -> set.get(Strings.parseInt(str)))
.growX().fillY()
.valid(Strings::canParseInt)
.get().setFilter(TextFieldFilter.digitsOnly);
});
setProvider(float.class, (type, cons) -> cons.get(0f));
setInterpreter(float.class, (cont, name, type, field, remover, indexer, get, set) -> {
float m = 1f;
if(field != null){
if(field.isAnnotationPresent(Second.class)){
m = 60f;
}else if(field.isAnnotationPresent(TilePos.class)){
m = 8f;
}
}
float mult = m;
name(cont, name, remover, indexer);
cont.field(Float.toString(get.get() / mult), str -> set.get(Strings.parseFloat(str) * mult))
.growX().fillY()
.valid(Strings::canParseFloat)
.get().setFilter(TextFieldFilter.floatsOnly);
});
setProvider(UnlockableContent.class, (type, cons) -> cons.get(Blocks.coreShard));
setInterpreter(UnlockableContent.class, (cont, name, type, field, remover, indexer, get, set) -> {
name(cont, name, remover, indexer);
cont.table(t -> t.left().button(
b -> b.image().size(iconSmall).scaling(Scaling.fit).update(i -> i.setDrawable(get.get().uiIcon)),
() -> showContentSelect(null, set, b -> (field != null && !field.isAnnotationPresent(Researchable.class)) || b.techNode != null)
).fill().pad(4)).growX().fillY();
});
setProvider(Block.class, (type, cons) -> cons.get(Blocks.copperWall));
setInterpreter(Block.class, (cont, name, type, field, remover, indexer, get, set) -> {
name(cont, name, remover, indexer);
cont.table(t -> t.left().button(
b -> b.image().size(iconSmall).update(i -> i.setDrawable(get.get().uiIcon)),
() -> showContentSelect(ContentType.block, set, b -> (field != null && !field.isAnnotationPresent(Synthetic.class)) || b.synthetic())
).fill().pad(4f)).growX().fillY();
});
setProvider(Item.class, (type, cons) -> cons.get(Items.copper));
setInterpreter(Item.class, (cont, name, type, field, remover, indexer, get, set) -> {
name(cont, name, remover, indexer);
cont.table(t -> t.left().button(
b -> b.image().size(iconSmall).update(i -> i.setDrawable(get.get().uiIcon)),
() -> showContentSelect(ContentType.item, set, item -> true)
).fill().pad(4f)).growX().fillY();
});
setProvider(UnitType.class, (type, cons) -> cons.get(UnitTypes.dagger));
setInterpreter(UnitType.class, (cont, name, type, field, remover, indexer, get, set) -> {
name(cont, name, remover, indexer);
cont.table(t -> t.left().button(
b -> b.image().size(iconSmall).update(i -> i.setDrawable(get.get().uiIcon)),
() -> showContentSelect(ContentType.unit, set, unit -> true)
).fill().pad(4f)).growX().fillY();
});
setProvider(Team.class, (type, cons) -> cons.get(Team.sharded));
setInterpreter(Team.class, (cont, name, type, field, remover, indexer, get, set) -> {
name(cont, name, remover, indexer);
cont.table(t -> t.left().button(
b -> b.image(Tex.whiteui).size(iconSmall).update(i -> i.setColor(get.get().color)),
() -> showTeamSelect(set)
).fill().pad(4f)).growX().fillY();
});
setProvider(Color.class, (type, cons) -> cons.get(Pal.accent.cpy()));
setInterpreter(Color.class, (cont, name, type, field, remover, indexer, get, set) -> {
var out = get.get();
name(cont, name, remover, indexer);
cont.table(t -> t.left().button(
b -> b.stack(new Image(Tex.alphaBg), new Image(Tex.whiteui){{
update(() -> setColor(out));
}}).grow(),
Styles.squarei,
() -> ui.picker.show(out, res -> set.get(out.set(res)))
).margin(4f).pad(4f).size(50f)).growX().fillY();
});
setProvider(Vec2.class, (type, cons) -> cons.get(new Vec2()));
setInterpreter(Vec2.class, (cont, name, type, field, remover, indexer, get, set) -> {
var obj = get.get();
name(cont, name, remover, indexer);
cont.table(t -> {
boolean isInt = type.raw == int.class;
FieldInterpreter in = getInterpreter(float.class);
if(isInt) in = getInterpreter(int.class);
in.build(
t, "x", new TypeInfo(isInt ? int.class : float.class),
field, null, null,
isInt ? () -> (int)obj.x : () -> obj.x,
res -> {
obj.x = isInt ? (Integer)res : (Float)res;
set.get(obj);
}
);
in.build(
t.row(), "y", new TypeInfo(isInt ? int.class : float.class),
field, null, null,
isInt ? () -> (int)obj.y : () -> obj.y,
res -> {
obj.y = isInt ? (Integer)res : (Float)res;
set.get(obj);
}
);
}).growX().fillY();
});
setProvider(Point2.class, (type, cons) -> cons.get(new Point2()));
setInterpreter(Point2.class, (cont, name, type, field, remover, indexer, get, set) -> {
var obj = get.get();
var vec = new Vec2(obj.x, obj.y);
getInterpreter(Vec2.class).build(
cont, name, new TypeInfo(int.class),
field, remover, indexer,
() -> vec,
res -> {
vec.set(res);
set.get(obj.set((int)vec.x, (int)vec.y));
}
);
});
// Types that have a provider, but delegate to the default interpreter.
setProvider(MapObjective.class, (type, cons) -> new BaseDialog("@add"){{
cont.pane(p -> {
p.background(Tex.button);
p.marginRight(14f);
p.defaults().size(195f, 56f);
int i = 0;
for(var gen : MapObjectives.allObjectiveTypes){
var obj = gen.get();
p.button(obj.typeName(), Styles.flatt, () -> {
cons.get(obj);
hide();
}).with(Table::left).get().getLabelCell().growX().left().padLeft(5f).labelAlign(Align.left);
if(++i % 3 == 0) p.row();
}
}).scrollX(false);
addCloseButton();
show();
}});
setProvider(ObjectiveMarker.class, (type, cons) -> new BaseDialog("@add"){{
cont.pane(p -> {
p.background(Tex.button);
p.marginRight(14f);
p.defaults().size(195f, 56f);
int i = 0;
for(var gen : MapObjectives.allMarkerTypes){
var marker = gen.get();
p.button(marker.typeName(), Styles.flatt, () -> {
cons.get(marker);
hide();
}).with(Table::left).get().getLabelCell().growX().left().padLeft(5f).labelAlign(Align.left);
if(++i % 3 == 0) p.row();
}
}).scrollX(false);
addCloseButton();
show();
}});
setInterpreter(Vertices.class, float[].class, (cont, name, type, field, remover, indexer, get, set) -> cont.table(main -> {
float[] data = get.get();
name(cont, name, remover, indexer);
cont.table(t -> {
t.left().defaults().left();
String[] names = {"x", "y", "color", "u", "v"};
int stride = 6;
int vertices = data.length / stride;
for(int i = 0; i < vertices; i++){
int offset = i * stride;
t.table(row -> {
for(int j = 0; j < names.length; j++){
int index = offset + j;
if("color".equals(names[j])) {
getInterpreter(Color.class).build(row, names[j], new TypeInfo(Color.class), null, null, null, () -> new Color().abgr8888(data[index]), value -> data[index] = value.toFloatBits());
}else{
float scale = j <= 1 ? tilesize : 1;
getInterpreter(float.class).build(row, names[j], new TypeInfo(float.class), null, null, null, () -> data[index] / scale, value -> data[index] = value * scale);
}
row.add().pad(4);
}
}).row();
}
});
}));
// Types that use the default interpreter. It would be nice if all types could use it, but I don't know how to reliably prevent classes like [? extends Content] from using it.
for(var obj : MapObjectives.allObjectiveTypes) setInterpreter(obj.get().getClass(), defaultInterpreter());
for(var mark : MapObjectives.allMarkerTypes) setInterpreter(mark.get().getClass(), defaultInterpreter());
// Annotated field interpreters.
setInterpreter(LabelFlag.class, byte.class, (cont, name, type, field, remover, indexer, get, set) -> {
name(cont, name, remover, indexer);
cont.table(t -> {
t.left().defaults().left();
byte
value = get.get(),
bg = WorldLabel.flagBackground, out = WorldLabel.flagOutline;
t.check("@marker.background", (value & bg) == bg, res -> set.get((byte)(res ? value | bg : value & ~bg)))
.growX().fillY()
.padTop(4f).padBottom(4f).get().getLabelCell().growX();
t.row();
t.check("@marker.outline", (value & out) == out, res -> set.get((byte)(res ? value | out : value & ~out)))
.growX().fillY().get().getLabelCell().growX();
}).growX().fillY();
});
setInterpreter(IndexBool.class, int.class, (cont, name, type, field, remover, indexer, get, set) -> {
getInterpreter(Boolean.class).build(cont, name, type, field, remover, indexer, () -> get.get() != -1, v -> set.get(v ? +1 : -1));
});
// Special data structure interpreters.
// Instantiate default `Seq`s with a reflectively allocated array.
setProvider(Seq.class, (type, cons) -> cons.get(new Seq<>(type.element.raw)));
setInterpreter(Seq.class, (cont, name, type, field, remover, indexer, get, set) -> cont.table(main -> {
Runnable[] rebuild = {null};
var arr = get.get();
main.margin(0f, 10f, 0f, 10f);
var header = main.table(Tex.button, t -> {
t.left();
t.margin(10f);
if(name.length() > 0) t.add(name + ":").color(Pal.accent);
t.add().growX();
if(remover != null) t.button(Icon.trash, Styles.emptyi, remover).fill().padRight(4f);
if(indexer != null){
t.button(Icon.upOpen, Styles.emptyi, () -> indexer.get(true)).fill().padRight(4f);
t.button(Icon.downOpen, Styles.emptyi, () -> indexer.get(false)).fill().padRight(4f);
}
if(!field.isAnnotationPresent(Immutable.class)) {
t.button(Icon.add, Styles.emptyi, () -> getProvider(type.element.raw).get(type.element, res -> {
arr.add(res);
rebuild[0].run();
})).fill();
}
}).growX().height(46f).pad(0f, -10f, 0f, -10f).get();
main.row().table(Tex.button, t -> rebuild[0] = () -> {
t.clear();
t.top();
if(arr.isEmpty()){
t.background(Tex.clear).margin(0f).setSize(0f);
}else{
t.background(Tex.button).margin(10f).marginTop(20f);
}
for(int i = 0, len = arr.size; i < len; i++){
int index = i;
if(index > 0) t.row();
getInterpreter((Class<Object>)arr.get(index).getClass()).build(
t, "", new TypeInfo(arr.get(index).getClass()),
field, field == null || !field.isAnnotationPresent(Immutable.class) ? () -> {
arr.remove(index);
rebuild[0].run();
} : null, field == null || !field.isAnnotationPresent(Unordered.class) ? in -> {
if(in && index > 0){
arr.swap(index, index - 1);
rebuild[0].run();
}else if(!in && index < len - 1){
arr.swap(index, index + 1);
rebuild[0].run();
}
} : null,
() -> arr.get(index),
res -> {
arr.set(index, res);
set.get(arr);
}
);
}
set.get(arr);
}).padTop(-10f).growX().fillY();
rebuild[0].run();
header.toFront();
}).growX().fillY().pad(4f).colspan(2));
// Reserved for array types that are not explicitly handled. Essentially handles it the same way as `Seq`.
setProvider(Object[].class, (type, cons) -> cons.get(Reflect.newArray(type.element.raw, 0)));
setInterpreter(Object[].class, (cont, name, type, field, remover, indexer, get, set) -> {
var arr = Seq.with(get.get());
getInterpreter(Seq.class).build(
cont, name, new TypeInfo(Seq.class, type.element),
field, remover, indexer,
() -> arr,
res -> set.get(arr.toArray(type.element.raw))
);
});
}
public static <T> FieldInterpreter<T> defaultInterpreter(){
return (cont, name, type, field, remover, indexer, get, set) -> cont.table(main -> {
main.margin(0f, 10f, 0f, 10f);
var header = main.table(Tex.button, t -> {
t.left();
t.margin(10f);
if(name.length() > 0) t.add(name + ":").color(Pal.accent);
t.add().growX();
Cell<ImageButton> remove = null;
if(remover != null) remove = t.button(Icon.trash, Styles.emptyi, remover).fill();
if(indexer != null){
if(remove != null) remove.padRight(4f);
t.button(Icon.upOpen, Styles.emptyi, () -> indexer.get(true)).fill().padRight(4f);
t.button(Icon.downOpen, Styles.emptyi, () -> indexer.get(false)).fill();
}
}).growX().height(46f).pad(0f, -10f, -0f, -10f).get();
main.row().table(Tex.button, t -> {
t.left();
t.top().margin(10f).marginTop(20f);
t.defaults().minHeight(40f).left();
var obj = get.get();
int i = 0;
for(var e : JsonIO.json.getFields(type.raw).values()){
if(i++ > 0) t.row();
var f = e.field;
var ft = f.getType();
int mods = f.getModifiers();
if(!Modifier.isPublic(mods) || (Modifier.isFinal(mods) && (
String.class.isAssignableFrom(ft) ||
unbox(ft).isPrimitive()
))) continue;
var anno = Structs.find(f.getDeclaredAnnotations(), a -> hasInterpreter(a.annotationType(), ft));
getInterpreter(anno == null ? Override.class : anno.annotationType(), ft).build(
t, f.getName(), new TypeInfo(f),
f, null, null,
() -> Reflect.get(obj, f),
Modifier.isFinal(mods) ? res -> {} : res -> Reflect.set(obj, f, res)
);
}
}).padTop(-10f).growX().fillY();
header.toFront();
}).growX().fillY().pad(4f).colspan(2);
}
public static void name(Table cont, CharSequence name, @Nullable Runnable remover, @Nullable Boolc indexer){
if(indexer != null || remover != null){
cont.table(t -> {
if(remover != null) t.button(Icon.trash, Styles.emptyi, remover).fill().padRight(4f);
if(indexer != null){
t.button(Icon.upOpen, Styles.emptyi, () -> indexer.get(true)).fill().padRight(4f);
t.button(Icon.downOpen, Styles.emptyi, () -> indexer.get(false)).fill().padRight(4f);
}
}).fill();
}else{
cont.add(name + ": ");
}
}
public MapObjectivesDialog(){
super("@editor.objectives");
clear();
margin(0f);
stack(
new Image(Styles.black5),
canvas = new MapObjectivesCanvas(),
new Table(){{
buttons.defaults().size(160f, 64f).pad(2f);
buttons.button("@back", Icon.left, MapObjectivesDialog.this::hide);
buttons.button("@add", Icon.add, () -> getProvider(MapObjective.class).get(new TypeInfo(MapObjective.class), canvas::query));
buttons.button("@waves.edit", Icon.edit, () -> {
BaseDialog dialog = new BaseDialog("@waves.edit");
dialog.addCloseButton();
dialog.setFillParent(false);
dialog.cont.table(Tex.button, t -> {
var style = Styles.cleart;
t.defaults().size(280f, 64f).pad(2f);
t.button("@waves.copy", Icon.copy, style, () -> {
ui.showInfoFade("@copied");
Core.app.setClipboardText(JsonIO.write(new MapObjectives(canvas.objectives)));
dialog.hide();
}).disabled(b -> canvas.objectives.isEmpty()).marginLeft(12f).row();
t.button("@waves.load", Icon.download, style, () -> {
try{
rebuildObjectives(new Seq<>(JsonIO.read(MapObjectives.class, Core.app.getClipboardText()).all));
}catch(Exception e){
Log.err(e);
ui.showErrorMessage("@waves.invalid");
}
dialog.hide();
}).disabled(Core.app.getClipboardText() == null || !Core.app.getClipboardText().startsWith("[")).marginLeft(12f).row();
t.button("@clear", Icon.none, style, () -> ui.showConfirm("@confirm", "@settings.clear.confirm", () -> {
rebuildObjectives(new Seq<>());
dialog.hide();
})).marginLeft(12f).row();
});
dialog.show();
});
if(mobile){
buttons.button("@cancel", Icon.cancel, canvas::stopQuery).visible(() -> canvas.isQuerying());
buttons.button("@ok", Icon.ok, canvas::placeQuery).visible(() -> canvas.isQuerying());
}
setFillParent(true);
margin(3f);
add(titleTable).growX().fillY();
row().add().grow();
row().add(buttons).fill();
addCloseListener();
}}
).grow().pad(0f).margin(0f);
hidden(() -> {
out.get(canvas.objectives);
out = arr -> {};
});
}
public void show(Seq<MapObjective> objectives, Cons<Seq<MapObjective>> out){
this.out = out;
rebuildObjectives(objectives);
show();
}
public void rebuildObjectives(Seq<MapObjective> objectives){
canvas.clearObjectives();
if(
objectives.any() && (
// If the objectives were previously programmatically made...
objectives.contains(obj -> obj.editorX == -1 || obj.editorY == -1) ||
// ... or some idiot somehow made it not work...
objectives.contains(obj -> !canvas.tilemap.createTile(obj))
)){
// ... then rebuild the structure.
canvas.clearObjectives();
// This is definitely NOT a good way to do it, but only insane people or people from the distant past would actually encounter this anyway.
int w = objWidth + 2,
len = objectives.size * w,
columns = objectives.size,
rows = 1;
if(len > bounds){
rows = len / bounds;
columns = bounds / w;
}
int i = 0;
loop:
for(int y = 0; y < rows; y++){
for(int x = 0; x < columns; x++){
if(canvas.tilemap.createTile(x * w, y, objectives.get(i))){
i++;
}
if(i >= objectives.size) break loop;
}
}
}
canvas.objectives.set(objectives);
}
public static <T extends UnlockableContent> void showContentSelect(@Nullable ContentType type, Cons<T> cons, Boolf<T> check){
BaseDialog dialog = new BaseDialog("");
dialog.cont.pane(Styles.noBarPane, t -> {
int i = 0;
for(var content : (type == null ? content.blocks().copy().<UnlockableContent>as()
.add(content.items())
.add(content.liquids())
.add(content.units()) :
content.getBy(type).<UnlockableContent>as()
)){
if(content.isHidden() || !check.get((T)content)) continue;
t.image(content == Blocks.air ? Icon.none.getRegion() : content.uiIcon).size(iconMed).pad(3).scaling(Scaling.fit)
.with(b -> b.addListener(new HandCursorListener()))
.tooltip(content.localizedName).get().clicked(() -> {
cons.get((T)content);
dialog.hide();
});
if(++i % 10 == 0) t.row();
}
}).fill();
dialog.closeOnBack();
dialog.show();
}
public static void showTeamSelect(Cons<Team> cons){
BaseDialog dialog = new BaseDialog("");
for(var team : Team.baseTeams){
dialog.cont.image(Tex.whiteui).size(iconMed).color(team.color).pad(4)
.with(i -> i.addListener(new HandCursorListener()))
.tooltip(team.localized()).get().clicked(() -> {
cons.get(team);
dialog.hide();
});
}
dialog.closeOnBack();
dialog.show();
}
public static Class<?> unbox(Class<?> boxed){
return switch(boxed.getSimpleName()){
case "Boolean" -> boolean.class;
case "Byte" -> byte.class;
case "Character" -> char.class;
case "Short" -> short.class;
case "Integer" -> int.class;
case "Long" -> long.class;
case "Float" -> float.class;
case "Double" -> double.class;
default -> boxed;
};
}
public static <T> void setInterpreter(Class<T> type, FieldInterpreter<? super T> interpreter){
setInterpreter(Override.class, type, interpreter);
}
public static <T> void setInterpreter(Class<? extends Annotation> anno, Class<T> type, FieldInterpreter<? super T> interpreter){
interpreters.get(anno, ObjectMap::new).put(type, interpreter);
}
public static boolean hasInterpreter(Class<?> type){
return hasInterpreter(Override.class, type);
}
public static boolean hasInterpreter(Class<? extends Annotation> anno, Class<?> type){
return interpreters.get(anno, ObjectMap::new).containsKey(unbox(type));
}
public static <T> FieldInterpreter<T> getInterpreter(Class<T> type){
return getInterpreter(Override.class, type);
}
public static <T> FieldInterpreter<T> getInterpreter(Class<? extends Annotation> anno, Class<T> type){
if(hasInterpreter(anno, type)){
return (FieldInterpreter<T>)interpreters.get(anno, ObjectMap::new).get(unbox(type));
}else if(hasInterpreter(Override.class, type)){
return (FieldInterpreter<T>)interpreters.get(Override.class, ObjectMap::new).get(unbox(type));
}else if(type.isArray() && !type.getComponentType().isPrimitive()){
return (FieldInterpreter<T>)(hasInterpreter(anno, Object[].class)
? interpreters.get(anno).get(Object[].class)
: interpreters.get(Override.class).get(Object[].class)
);
}else{
throw new IllegalArgumentException("Interpreter for type " + type + " not set up yet.");
}
}
public static <T> void setProvider(Class<T> type, FieldProvider<T> provider){
providers.put(unbox(type), provider);
}
public static boolean hasProvider(Class<?> type){
return providers.containsKey(unbox(type));
}
public static <T> FieldProvider<T> getProvider(Class<T> type){
return (FieldProvider<T>)providers.getThrow(unbox(type), () -> new IllegalArgumentException("Provider for type " + type + " not set up yet."));
}
public interface FieldInterpreter<T>{
/**
* Builds the interpreter for (not-necessarily) a possibly annotated field. Implementations must add exactly
* 2 columns to the table.
* @param name May be empty.
* @param remover If this callback is not {@code null}, this interpreter should add a button that invokes the
* callback to signal element removal.
* @param indexer If this callback is not {@code null}, this interpreter should add 2 buttons that invoke the
* callback to signal element rearrangement with the following values:<ul>
* <li>{@code true}: Swap element with previous index.</li>
* <li>{@code false}: Swap element with next index.</li>
* </ul>
*/
void build(Table cont,
CharSequence name, TypeInfo type,
@Nullable Field field,
@Nullable Runnable remover, @Nullable Boolc indexer,
Prov<T> get, Cons<T> set);
}
public interface FieldProvider<T>{
void get(TypeInfo type, Cons<T> cons);
}
/**
* Stores parameterized or array type information for convenience.
* For {@code A[]}: {@link #raw} is {@code A[]}, {@link #element} is {@code A}, {@link #key} is {@code null}.
* For {@code Seq<A>}: {@link #raw} is {@link Seq}, {@link #element} is {@code A}, {@link #key} is {@code null}.
* For {@code ObjectMap<A, B>}: {@link #raw} is {@link ObjectMap}, {@link #element} is {@code B}, {@link #key} is {@code A}.
*/
public static class TypeInfo{
public final Class<?> raw;
public final TypeInfo element, key;
public TypeInfo(Field field){
this(field.getType(), field.getGenericType());
}
public TypeInfo(Class<?> raw){
this(raw, raw);
}
/** Use with care! */
public TypeInfo(Class<?> raw, TypeInfo element){
this.raw = unbox(raw);
this.element = element;
key = null;
}
public TypeInfo(Class<?> raw, Type generic){
this.raw = unbox(raw);
if(raw.isArray()){
key = null;
element = new TypeInfo(raw.getComponentType(), generic instanceof GenericArrayType type ? type.getGenericComponentType() : raw.getComponentType());
}else if(Seq.class.isAssignableFrom(raw)){
key = null;
element = getParam(generic, 0);
}else if(ObjectMap.class.isAssignableFrom(raw)){
key = getParam(generic, 0);
element = getParam(generic, 1);
}else{
key = element = null;
}
}
public static TypeInfo getParam(Type generic, int index){
Type[] params =
generic instanceof ParameterizedType type ? type.getActualTypeArguments() :
generic instanceof GenericDeclaration type ? type.getTypeParameters() : null;
if(params != null && index < params.length){
var target = params[index];
return new TypeInfo(raw(target), target);
}
return new TypeInfo(Object.class, Object.class);
}
public static Class<?> raw(Type type){
if(type instanceof Class<?> c) return c;
if(type instanceof ParameterizedType c) return (Class<?>)c.getRawType();
if(type instanceof GenericArrayType c) return Reflect.newArray(raw(c.getGenericComponentType()), 0).getClass();
if(type instanceof TypeVariable<?> c) return raw(c.getBounds()[0]);
return Object.class;
}
}
}