Extending Lienzo
From Emitrom
| |
To add a new Shape class:
- extend the class from Shape
- add new attribute names (if necessary)
- add new attribute types (if necessary)
- write Javascript wrapper class for new attribute type (if necessary)
- add getters/setters for its attributes
- implement
drawmethod - add support for copying and Serialization
- add an IFactory
- register IFactory with FactoryRegistry
- add getFactory method to Shape class
See Node Attributes
Contents |
Example: Pear Shape
To illustrate the process we'll add a fictitious Pear shape. The Pear has two properties: "pearWidth", which is a double and "pearity", which is represented by the Java object "Pearity". The Javascript object that represents a "Pearity" has the following attributes: "flavor" (string), "leafCount" (number) and "leafColor" (color String).
Somehow these attributes describe the Pear's pearness and influence how the Pear will be drawn :-)
Extend class from Shape
public class Pear extends Shape<Pear>
{
public static final ShapeType PEAR_SHAPE = new ShapeType("Pear");
}
"Pear" is used as the shape type identifier when serializing the shape.
Look at ShapeType Javadoc and NodeType Javadoc for the type names used by Lienzo, to avoid conflicts.
Add Attribute name
The Attribute class is a type-safe, extensible enumeration of all possible Node attributes.
When adding a new Shape class, it may be necessary to define new Attributes.
To add a new Attribute for PEAR_WIDTH and PEARITY we need to create two static variables (anywhere will do, we'll put them inside the Pear class):
public final static Attribute PEAR_WIDTH = new Attribute("pearWidth", "pearWidthLabel",
"pearWidthDescription", AttributeType.NUMBER_TYPE);
public final static Attribute PEARITY = new Attribute("pearity", "pearityLabel",
"pearityDescription", Pear.PEARITY_TYPE);
The first argument will be used internally as the attribute key. It is also used when deserializing the node to JSON.
The 2nd and 3rd value (label and description) values are placeholders for attribute sheets in bean editors. These values should be internationalized if you want to share your new class with international users.
The last argument describes what AttributeType the values of the new attribute have.
Add AttributeType
When adding a new Attribute, it may be necessary to add a new AttributeType (but try to use the existing AttributeTypes if possible.)
AttributeType is a type-safe, extensible enumeration of attribute types for all possible Node attributes.
Here are some of the built-in AttributeTypes:
| AttributeType | Java Type | Javascript Type | Notes |
|---|---|---|---|
| STRING_TYPE | String | String | |
| NUMBER_TYPE | int, Integer, double, Double etc. | Number | |
| BOOLEAN_TYPE | boolean | Boolean | |
| COLOR_TYPE | String | String | Any valid CSS Color String. See Color. |
| POINT2D_TYPE | Point2D | Object with attributes: x (Number), y (Number) | |
| POINT2D_ARRAY | Point2DArray | Array with Point2D objects | |
| SHADOW_TYPE | Shadow | Object with attributes: color (String), blur (Number), offset (Number) | |
| DASH_ARRAY_TYPE | DashArray | Array of Numbers | |
| LINEAR_GRADIENT_TYPE | LinearGradient | Object with attributes: start, end, colorStops and type="LinearGradient" | start and end are Point2D objects. colorStops is an Array of Objects with stop (Number) and color (String) attributes. |
| PATTERN_GRADIENT_TYPE | PatternGradient | Object with attributes: image (TODO), repeat (see enum FillRepeat) and type="PatternGradient" | |
| RADIAL_GRADIENT_TYPE | RadialGradient | Object with attributes: start, end, colorStops and type="RadialGradient" | start and end are Objects with x (Number), y (Number) and radius (Number) attributes. colorStops is an Array of Objects with stop (Number) and color (String) attributes. |
| IMAGE_TYPE | |||
| DRAG_BOUNDS_TYPE | DragBounds | Object with attributes: x1, y1, x2, y2 (all Numbers) | |
| FILL_TYPE | Color, LinearGradient, PatternGradient or RadialGradient | see above | |
| STROKE_TYPE | String | String (same as COLOR_TYPE) |
Each enumeration has its own type (the internal value is the string value obtained with EnumWithValue.getValue):
| AttributeType | Enum | Javadoc |
|---|---|---|
| LINE_CAP_TYPE | LineCap | |
| LINE_JOIN_TYPE | LineJoin | |
| DRAG_CONSTRAINT_TYPE | DragConstraint | |
| TEXT_ALIGN_TYPE | TextAlign | |
| TEXT_BASELINE_TYPE | TextBaseLine | |
| COMPOSITE_OPERATION_TYPE | CompositeOperation | |
| ARROW_TYPE | ArrowType | |
Here is an example for adding the new AttributeType for the "pearity".
Pearity will need a custom validator. This is used during deserialization.
public static AttributeType PEARITY_TYPE = new AttributeType(new PearityValidator());
Write Javascript wrapper class
To represent the "pearity" type, we would probably write a Java class called "Pearity". It's a wrapper around a Javascript object. We normally create a separate class that extends JavaScriptObject (PearityJSO in this case.) See the Lienzo source code for examples on how to implement these classes (e.g. DragBounds).
public static class Pearity
{
private final PearityJSO m_jso;
public Pearity(PearityJSO jso)
{ ... }
public final PearityJSO getJSO()
{
return m_jso;
}
public static final class PearityJSO extends JavaScriptObject
{ ... }
}
Add getters and setters for new attributes
public Pear setPearity(Pearity pearity)
{
if (null == pearity)
getAttributes().delete(PEARITY.getProperty());
else
getAttributes.put(PEARITY.getProperty(), pearity.getJSO())
return cast();
}
public Pearity getPearity()
{
JavaScriptObject jso = getAttributes().getObject(PEARITY.getProperty());
if (null == jso)
return null;
else
return new Pearity(jso);
}
public Pear setPearWidth(double pearWidth)
{
getAttributes().put(PEAR_WIDTH.getProperty(), pearWidth);
return this;
}
public double getPearWidth()
{
return getAttributes().getDouble(PEAR_WIDTH.getProperty());
}
Implement draw method
The draw method in the Shape class should assume that the Transform related attributes and fill/stroke related attributes have already been applied to the Context2D.
Here is the draw method for the Circle class. Note that it draws the center at (0,0) because the Transform related attributes (such as X and Y) have already been applied to the Context2D.
public void draw(Context2D context)
{
context.beginPath();
context.arc(0, 0, getRadius(), 0, Math.PI * 2, true);
context.closePath();
}
If the new Node class is a type of ContainerNode, it should also draw its children here. See the source code for Group.draw() for an example. (Usually you don't need to derive from ContainerNode. Just use a Group with child nodes instead.)
Add support for copying and Serialization
By adding support for Serialization to JSON we get a copy method for free.
copy simply serializes to JSON and then deserializes the JSON into a new object.
Add type Validator
All we need to do is add validators for the new AttributeTypes, in this case PearityValidator which was used by PEARITY_TYPE above.
public class PearityValidator extends ObjectValidator
{
public PearityValidator ()
{
super("Pearity");
// the 3rd parameter indicates whether the attribute is required
addAttribute("flavor", StringValidator.INSTANCE, true);
addAttribute("leafCount", NumberValidator.INSTANCE, true);
addAttribute("leafColor", ColorValidator.INSTANCE, true);
}
}
Add an IFactory
public static class PearFactory extends ShapeFactory<Pear>
{
public PearFactory()
{
super(Pear.PEAR_SHAPE);
addAttribute(PEAR_WIDTH, true); // true indicates that it's a required attribute
addAttribute(PEARITY, true);
}
@Override
public Pear create(JSONObject node, ValidationContext ctx)
{
return new Pear(node);
}
}
If you're implementing a subclass of ContainerNode, extend the ContainerNodeFactory instead. This requires you to also implement the isValidForContainer method to restrict the which child nodes can be added.
Register IFactory with FactoryRegistry
And register the factory with FactoryRegistry:
FactoryRegistry registry = FactoryRegistry.getInstance(); registry.registerFactory(new Pear.PearFactory());
Add getFactory method
In the Pear class, add a method that returns the PearFactory:
@Override
public IFactory<Pear> getFactory()
{
return new PearFactory();
}
Also add the Pear constructor that takes a JSONObject.