package net.alcuria.umbracraft.editor.modules; import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.util.Set; import net.alcuria.umbracraft.annotations.IgnorePopulate; import net.alcuria.umbracraft.annotations.Order; import net.alcuria.umbracraft.annotations.Tooltip; import net.alcuria.umbracraft.definitions.Definition; import net.alcuria.umbracraft.editor.widget.SuggestionWidget; import net.alcuria.umbracraft.editor.widget.WidgetUtils; import net.alcuria.umbracraft.listeners.Listener; import net.alcuria.umbracraft.listeners.TypeListener; import com.badlogic.gdx.Gdx; import com.badlogic.gdx.files.FileHandle; import com.badlogic.gdx.scenes.scene2d.Event; import com.badlogic.gdx.scenes.scene2d.EventListener; import com.badlogic.gdx.scenes.scene2d.ui.Button; import com.badlogic.gdx.scenes.scene2d.ui.Table; import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener.ChangeEvent; import com.badlogic.gdx.scenes.scene2d.utils.ClickListener; import com.badlogic.gdx.utils.Align; import com.badlogic.gdx.utils.Array; import com.badlogic.gdx.utils.Json; import com.badlogic.gdx.utils.JsonWriter.OutputType; import com.badlogic.gdx.utils.ObjectMap; import com.badlogic.gdx.utils.Timer; import com.badlogic.gdx.utils.Timer.Task; import com.kotcrab.vis.ui.widget.VisCheckBox; import com.kotcrab.vis.ui.widget.VisLabel; import com.kotcrab.vis.ui.widget.VisSelectBox; import com.kotcrab.vis.ui.widget.VisTextButton; import com.kotcrab.vis.ui.widget.VisTextField; import com.kotcrab.vis.ui.widget.VisTextField.TextFieldListener; /** The base class of a module. Modules contain UI to modify {@link Definition} * classes. * @author Andrew Keturi * @param <T> */ @SuppressWarnings("unchecked") public abstract class Module<T extends Definition> { public static enum FieldType { FLOAT, INT, STRING; public static FieldType from(Field field) { if (field.getType().toString().equals("float")) { return FLOAT; } else if (field.getType().toString().equals("int")) { return INT; } return STRING; } } /** Same as a {@link Field}, but ordered based on an optional order value. * @author Andrew Keturi */ public class OrderedField implements Comparable<OrderedField> { public Field field; public int order; @Override public int compareTo(Module<T>.OrderedField o) { if (o.order == order) { return field.getName().compareTo(o.field.getName()); } else { return order - o.order; } } } /** A helper class to define layout configurations when populating * definition. * @author Andrew Keturi */ public static class PopulateConfig { /** Columns for each field */ public int cols = 3; /** If non-null, only displays field names included in this set */ public Set<String> filter; /** Width of the label */ public int labelWidth = 130; /** A custom listener to invoke when a field changes value */ public TypeListener<String> listener; /** Maps a field to an array of suggestions */ public ObjectMap<String, Array<String>> suggestions; /** Width of the text fields */ public int textFieldWidth = 100; } public VisTextButton button; protected T rootDefinition; public Module() { button = new VisTextButton(getTitle()); } public Button getButton() { return button; } public abstract String getTitle(); public void load(final Class<T> clazz) { Json json = new Json(); json.setIgnoreUnknownFields(true); final FileHandle handle = Gdx.files.external("umbracraft/" + getTitle().toLowerCase() + ".json"); if (handle.exists()) { rootDefinition = json.fromJson(clazz, handle); } else { try { rootDefinition = clazz.newInstance(); } catch (InstantiationException | IllegalAccessException e) { e.printStackTrace(); } } } public abstract void populate(Table content); /** A generic populate method. Populates a table with a class's fields which * may be modified. For ints and string fields, a textfield is created where * the value of the field is updated when the textfield changes. For boolean * fields a checkbox is used. For enums, a dropdown menu will be generated. * Other objects and private fields are not displayed. * @param content the table to update * @param clazz the class definitions * @param definition the definition we want to update (when fields change * and so on) * @param config configuration settings for how the populate is handled */ public void populate(final Table content, final Class<?> clazz, final Definition definition, final PopulateConfig config) { assert (config.cols > 0); try { content.add(new Table() { { int idx = 0; // get all fields Array<Field> fieldList = new Array<Field>(clazz.getDeclaredFields()); // sort by @Order annotation Array<OrderedField> orderedFields = new Array<OrderedField>(); for (int i = 0; i < fieldList.size; i++) { OrderedField ordered = new OrderedField(); final Order orderAnnotation = fieldList.get(i).getAnnotation(Order.class); ordered.order = orderAnnotation != null ? orderAnnotation.value() : Integer.MAX_VALUE; ordered.field = fieldList.get(i); orderedFields.add(ordered); } orderedFields.sort(); for (int i = 0; i < orderedFields.size; i++) { final Field field = orderedFields.get(i).field; if (field.getAnnotation(IgnorePopulate.class) == null && visible(field, config) && field.getModifiers() != Modifier.PRIVATE && (field.getType().isEnum() || field.getType().toString().equals("int") || field.getType().toString().equals("float") || field.getType() == String.class || field.getType().toString().equals("boolean"))) { if (idx % config.cols == config.cols - 1) { add(keyInput(definition, field)).row(); } else { add(keyInput(definition, field)); } idx++; } } int size = fieldList.size; while (size % config.cols != 0) { add(); size++; } } private Table keyInput(final Definition definition, final Field field) { return new Table() { { // check if we should show a tooltip for this field final Tooltip annotation = field.getAnnotation(Tooltip.class); if (annotation != null) { add(WidgetUtils.tooltip(annotation.value())); } else { add().size(24); } if (field.getType().isEnum()) { add(new VisLabel(field.getName())).minWidth(config.labelWidth); final VisSelectBox selectBox = new VisSelectBox(); selectBox.setItems(field.getType().getEnumConstants()); add(selectBox).width(config.textFieldWidth).expandX().fill().left(); try { selectBox.setSelected(field.get(definition)); } catch (Exception e) { } selectBox.addListener(selected(selectBox, definition, field)); } else if (field.getType().toString().equals("boolean")) { add(new VisLabel(field.getName())).minWidth(config.labelWidth); boolean value = false; try { value = Boolean.valueOf(field.getBoolean(definition)); } catch (Exception e) { } final VisCheckBox checkBox = new VisCheckBox("", value); checkBox.addListener(new ClickListener() { @Override public void clicked(com.badlogic.gdx.scenes.scene2d.InputEvent event, float x, float y) { try { field.setBoolean(definition, checkBox.isChecked()); if (config.listener != null) { config.listener.invoke(field.getName()); } } catch (IllegalArgumentException | IllegalAccessException e) { e.printStackTrace(); } }; }); checkBox.align(Align.left); add(checkBox).width(config.textFieldWidth).expandX().fill().left(); } else if (field.getType().toString().equals("int") || field.getType().toString().equals("float") || field.getType() == String.class) { add(new VisLabel(field.getName())).minWidth(config.labelWidth); String value; try { value = String.valueOf(field.get(definition)); } catch (Exception e) { value = ""; } if ("null".equals(value)) { value = ""; } VisTextField textField = null; SuggestionWidget widget = null; if (config.suggestions != null && config.suggestions.containsKey(field.getName())) { widget = new SuggestionWidget(config.suggestions.get(field.getName()), config.textFieldWidth, true); add(widget.getActor()).width(config.textFieldWidth); textField = widget.getTextField(); textField.setText(value); final VisTextField tf = textField; final FieldType type = FieldType.from(field); widget.addSelectListener(new Listener() { @Override public void invoke() { saveField(type, field, definition, tf, config); } }); } else { textField = new VisTextField(value); add(textField).width(config.textFieldWidth); } final SuggestionWidget w = widget; final FieldType type = FieldType.from(field); final VisTextField textRef = textField; final Task saveTask = new Task() { @Override public void run() { saveField(type, field, definition, textRef, config); } }; textField.setTextFieldListener(new TextFieldListener() { @Override public void keyTyped(final VisTextField textField, char c) { if (c == '\t') { return; } Timer.instance().clear(); Timer.schedule(saveTask, 0.5f); if (w != null) { w.populateSuggestions(); } } }); } } }; } private EventListener selected(final VisSelectBox selectBox, final Definition definition, final Field field) { return new EventListener() { @Override public boolean handle(Event event) { if (event instanceof ChangeEvent) { try { field.set(definition, selectBox.getSelected()); } catch (IllegalArgumentException | IllegalAccessException e) { e.printStackTrace(); } if (config.listener != null) { config.listener.invoke(field.getName()); } return true; } return false; } }; } /** Determines whether or not a field should be visible based on * filteration. * @param field field we're inspecting * @param config the PopulateConfig * @return true if we should show it */ private boolean visible(Field field, PopulateConfig config) { if (config.filter == null) { return true; } return config.filter.contains(field.getName()); } }).expandX().fill(); } catch (Exception e) { e.printStackTrace(); } } public void save() { Json json = new Json(); json.setOutputType(OutputType.json); String jsonStr = json.prettyPrint(rootDefinition); Gdx.files.external("umbracraft/" + getTitle().toLowerCase() + ".json").writeString(jsonStr, false); } public void saveField(FieldType type, Field field, Definition definition, VisTextField textField, PopulateConfig config) { try { if (type == FieldType.INT) { if (textField.getText().equals("")) { field.setInt(definition, 0); } else { field.setInt(definition, Integer.valueOf(textField.getText())); } } else if (type == FieldType.FLOAT) { if (textField.getText().equals("")) { field.setFloat(definition, 0); } else { field.setFloat(definition, Float.valueOf(textField.getText())); } } else { field.set(definition, textField.getText()); } if (config.listener != null) { config.listener.invoke(field.getName()); } } catch (IllegalArgumentException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (NullPointerException npe) { System.out.println("null pointer"); } } }