New feature for SDK: AbstractControl + SerializableClass

Hi guys,
here is an example of using the SerializableClass annotation. In some cases I need to encapsulate the configuration parameters of a certain application in a dedicated class. These parameters I then need to pass them to a Builder class in order to build objects or perform some calculations. Taking advantage of the functionality of the SDK and AbstractControls, we could take a cue from Unity, using an annotation to be able to access fields in a class that does not extend the AbstractControl class. This way I can keep Parameters and Builders separate from AbstractControls that act as Editors.

  1. Main.java
package com.test.ui.model;

import java.beans.IntrospectionException;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.List;

import javax.swing.JButton;
import javax.swing.JLabel;
import javax.swing.JPanel;

import org.apache.commons.lang3.reflect.FieldUtils;
import org.apache.commons.lang3.reflect.MethodUtils;

import com.jme3.app.SimpleApplication;
import com.jme3.math.ColorRGBA;
import com.jme3.scene.control.Control;
import com.jme3.system.AppSettings;

import jme.capdevon.ui.annotations.ButtonProperty;
import jme.capdevon.ui.annotations.SerializableClass;

public class Test_SerializableClass extends SimpleApplication {

    /**
     * @param args
     */
    public static void main(String[] args) {
        AppSettings settings = new AppSettings(true);
        settings.setResolution(640, 480);

        Test_SerializableClass app = new Test_SerializableClass();
        app.setShowSettings(false);
        app.setPauseOnLostFocus(false);
        app.setSettings(settings);
        app.start();
    }

    @Override
    public void simpleInitApp() {
        viewPort.setBackgroundColor(new ColorRGBA(0.5f, 0.6f, 0.7f, 1.0f));
        rootNode.addControl(new TreeEditorComponent());
        
        for (int i = 0; i < rootNode.getNumControls(); i++) {
            Control control = rootNode.getControl(i);
            buildUIPanel(control);
        }
    }
    
    private JPanel buildUIPanel(Control control) {

        JPanel container = new JPanel();
        
        System.out.println(control.getClass());
        Field[] fields = FieldUtils.getAllFields(control.getClass());
        for (Field field : fields) {
            System.out.println("\t" + field);
            addUIComponent(control, field, container);
        }
        
        List<Method> methods = MethodUtils.getMethodsListWithAnnotation(control.getClass(), ButtonProperty.class);
        for (Method method : methods) {
            ButtonProperty bp = method.getAnnotation(ButtonProperty.class);
            JButton button = new JButton(bp.name());
            button.setToolTipText(bp.tooltip());
            button.addActionListener(e -> this.enqueue(() -> {
                try {
                    method.invoke(control);
                } catch (ReflectiveOperationException ex) {
                    ex.printStackTrace();
                }
            }));
            
            container.add(new JLabel(""), "align righ");
            container.add(button, "wrap, pushx, growx");
        }
        
        return container;
    }
    
    private void addUIComponent(Object bean, Field field, JPanel panel) {
        
        String propertyName = field.getName();
        Class<?> fieldType = field.getType();
        
        if (fieldType.getAnnotation(SerializableClass.class) != null) {
            System.out.println("\t--SerializableClass: " + fieldType);
            Object value = getValueOf(propertyName, bean);
            Field[] fields = FieldUtils.getAllFields(value.getClass());
            for (Field fd : fields) {
                System.out.println("\t\t--" + fd);
                addUIComponent(value, fd, panel);
            }
        } else {
            JComponent aComponent = ...;
            panel.add(new JLabel(propertyName), "align righ");
            panel.add(aComponent, "wrap, pushx, growx");
        }
    }
    
    private static Object getValueOf(String propertyName, Object bean) {
        try {
            PropertyDescriptor pd = new PropertyDescriptor(propertyName, bean.getClass());
            return pd.getReadMethod().invoke(bean);

        } catch (ReflectiveOperationException | IntrospectionException e) {
            throw new RuntimeException(e);
        }
    }

}
  1. TreeEditorComponent.java
public class TreeEditorComponent extends AbstractControl {
    
    private TreeSettings buildSettings = new TreeSettings();
    
    @ButtonProperty(name="Generate", tooltip="Procedural Vegetation Placement")
    public void generateTrees() {
        TreeBuilder builder = new TreeBuilder();
        builder.buildTrees(buildSettings);
    }

    @Override
    protected void controlUpdate(float tpf) {
    }

    @Override
    protected void controlRender(RenderManager rm, ViewPort vp) {
    }

    public TreeSettings getBuildSettings() {
        return buildSettings;
    }

    public void setBuildSettings(TreeSettings buildSettings) {
        this.buildSettings = buildSettings;
    }

}
  1. TreeBuilder.java
public class TreeBuilder {
    
    public void buildTrees(TreeSettings settings) {
        System.out.println("Generate trees with settings: " + settings);
    }

}
  1. TreeSettings.java
@SerializableClass
public class TreeSettings {

    private float cellSize = 1f;
    private float cellHeight = 1.5f;
    private float minTraversableHeight = 7.5f;
    private float maxTraversableStep = 1f;
    private float maxTraversableSlope = 48.0f;

    // getters & setters

}
  1. SerializableClass.java
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface SerializableClass {

}

Here is the output:

class com.test.ui.model.TreeEditorComponent
	private com.test.ui.model.TreeSettings com.test.ui.model.TreeEditorComponent.buildSettings
	--SerializableClass: class com.test.ui.model.TreeSettings
		--private float com.test.ui.model.TreeSettings.cellSize
		--private float com.test.ui.model.TreeSettings.cellHeight
		--private float com.test.ui.model.TreeSettings.minTraversableHeight
		--private float com.test.ui.model.TreeSettings.maxTraversableStep
		--private float com.test.ui.model.TreeSettings.maxTraversableSlope
	protected boolean com.jme3.scene.control.AbstractControl.enabled
	protected com.jme3.scene.Spatial com.jme3.scene.control.AbstractControl.spatial

Here is the result with java-swing

I hope it is useful as an example. Please let me know what you think about it.
@rickard @tonihele

Thank you.

5 Likes

In order for your code and the SDK to use some annotation, there needs to be a dependency to the said annotation. Where is the annotation going to be? Your app is going to depend jME for sure. But jME doesn’t want to host any annotations that doesn’t do anything for it. That means that it would have to go to the SDK. In which case your app would depend on the SDK. That I don’t think is really ideal. At least the annotations would need to be like a separate library hosted by the SDK. Containing only the annotations.

In Unity this is not a problem I suppose since it looks to me that the engine, your app and the editor is really tightly knit monolith. But normally you would not want this. I maybe wrong on this one since my experience with Unity is limited and obsolete at this point.

Maybe I misunderstood this. My biggest concern are the dependencies. Or this this just me OCDing…?

3 Likes

Hi @tonihele,

Correct. I was thinking of a separate library containing only the annotations, to which other classes could be added in the future to expand the functionality of the SDK. For those reasons I was thinking of a module named jme3-sdk-plugins or jme3-sdk-tools or jme3-sdk-devkit.

Yes, the library should belong to the SDK and not to the engine, and maybe it should be included in the list of default libraries.

Surely it would be useful to publish it to Maven Repository as a separate module so you can add it to the project via maven or gradle.
eg:

dependencies {
    implementation 'org.jmonkeyengine:jme3-sdk-xxx:1.0.0'
}

I strongly agree with your point of view.

1 Like