Last updated on 2014-09-07
Previous Tutorial: Creating a GEF Editor – Part 12: Enable Save Action on the Editor
In this tutorial we’ll be adding more link kinds to our editor. OPM has to main link kinds:
- Procedural Links: links that denote some kind of procedural relation between two things in the diagram. They normally connect between an Object and a Process.
- Structural Links: links that denote some kind of structural relation between two things in the diagram.
Procedural links are easy to add since they act like normal links just with small changes in the decorations that the links have. Structural links are more complicated and a full tutorial will be dedicated to adding them.
The hardest part in adding things to the editor is doing changes in the model without breaking things because the EMF generator, although very advanced, has some drawbacks that leave a lot of work for the developer (like deleting generated classes of model entities that were deleted). It also doesn’t support refactoring, which is a big drawback. But still the added benefit of using it overcome the problems. While preparing for this tutorial I also prepared the model for structural links, so please ignore them.
But enough mumblings, lets get going!
- First we are going to do some small changes and additions to the model. In OPM there are a number of entities that can be connected with links, and we only modeled this in things. Therefore we’ll create a new
EClass
that will be supertype of all classes that can have connections. We’ll call this classOPMNode
and transfer theincomingLinks
andoutgoingLinks
references to this class. Furthermore, we have to update theOPMThing
class so that it is now derived fromOPMNode
which lets usa remove the localincomingLinks
andoutgoingLinks
references). We must also update the type ofOPMLink
‘s references toOPMNode
. Furthermore, we add two more classes to our model:OPMProceduralLink
– which models a procedural link and extendsOPMLink
; andOPMStructuralLinkAggregator
– which will be used in the next tutorial to create structural links. Lastly, both structural links (actually the aggregators) and procedural links can be of many kinds, which is modeled by having akindenum
defined in the model (OPMStructuralLinkAggregatorKind
andOPMProceduralLinkKind
respectively).
A purist OOP programmer would say that procedural links should be implemented with inheritance, where every link kind would have a class of itself and would implements it’s own functionality. But from my view is that the most important this is understandability, and having so many entities in the model that are differentiated by very small functionality (as you will see later) is not good. Better to have a couple of smallswitch
statements in theOPMProceduralLink
than creating a larger hierarchy that confuses the reader of the code.
To save you some work (and me writing all of the specific changes here), the updated model file can be found here - After re-generating the code from the model, there are two editor classes that need modification:
OPMLinkDeleteCommand
uses aOPMThing
handle to store thesource
andtarget
nodes, but the newly generated classes returnOPMNode
instances, therefore we must change these references. A very similar thing happens inOPMThingDeleteCommand
, where the existing links are disconnected and their sources and targets are saved asOPMThing
instances, which must now be generalized toOPMNode
(note that at a later stage we should pass all commands and check which ones should be refactored to acceptOPMNode
instead ofOPMThing
and we should also probably create aOPMNodeEditPart
to abstain from duplicating code).
Since this changes are not too interesting they are not showed and the final files can be accessed here: OPMLinkDeleteCommand, OPMThingDeleteCommand. - Now for some real work. The agent and instrument procedural links are drawn like regular links only with a circle decoration at their target (black filled for the agent, white filled for the instrument). Since there is currently no circle decoration in GEF, we had to implement our own, which was very interesting stuff. A link decoration is a figure that can be rotated relative to a reference point, as prescribed in the
RotatableDecoration
interface. While a circle does not need rotation, the location of the circle in the diagram must be calculated when the link endpoint moves. Here is how I implemented it:package com.vainolo.phd.opm.gef.editor.figure; import org.eclipse.draw2d.Ellipse; import org.eclipse.draw2d.PolylineConnection; import org.eclipse.draw2d.RotatableDecoration; import org.eclipse.draw2d.geometry.Point; import org.eclipse.draw2d.geometry.Rectangle; /** * A decoration for {@link PolylineConnection} endpoints, which draws a circlE * whose center is located on the link and is distanced RADIUS (internal parameter) * units from the end of the connection. */ public class CircleDecoration extends Ellipse implements RotatableDecoration { private static final int RADIUS = 5; private Point location = new Point(); public CircleDecoration() { super(); } @Override public void setLocation(Point p) { location = p; Rectangle bounds = new Rectangle(location.x-RADIUS, location.y-RADIUS, RADIUS*2, RADIUS*2); setBounds(bounds); } @Override public void setReferencePoint(Point p) { // length of line between reference point and location double d = Math.sqrt(Math.pow((location.x-p.x), 2)+Math.pow(location.y-p.y,2)); // do nothing if link is too short. if(d < RADIUS) return; // // using pythagoras theorem, we have a triangle like this: // // | figure | // | | // |_____(l.x,l.y)______| // (\) // | \(r.x,r.y) // | |\ // | | \ // | | \ // | | \ // |_|(p.x,p.y) // // We want to find a point that at RADIUS distance from l (location) on the line between l and p // and center our circle at this point. // // I you remember your school math, let the distance between l and p (calculated // using pythagoras theorem) be defined as d. We want to find point r where the // distance between r and p is d-RADIUS (the same as saying that the distance // between l and r is RADIUS). We can do this using triangle identities. // |px-rx|/|px-lx|=|py-ry|/|py-ly|=|d-RADIUS|/d // // we use // k = |d-RADIUS|/d // longx = |px-lx| // longy = |py-xy| // // remember that d > RADIUS. // double k = (d-RADIUS)/d; double longx = Math.abs(p.x-location.x); double longy = Math.abs(p.y-location.y); double shortx = k*longx; double shorty = k*longy; // now create locate the new point using the distances depending on the location of the original points. int rx, ry; if(location.x < p.x) { rx = p.x - (int)shortx; } else { rx = p.x + (int)shortx; } if(location.y > p.y) { ry = p.y + (int)shorty; } else { ry = p.y - (int)shorty; } // For reasons that are still unknown to me, I had to increase the radius // of the circle for the graphics to look right. setBounds(new Rectangle(rx-RADIUS, ry-RADIUS, (int)(RADIUS*2.5), (int)(RADIUS*2.5))); } }
The final result is not completely to my expectations, but value for money it is good enough.
- All the other decorations that we need are provided by the framework, so we can now implement the
OPMProceduralLinkEditPart
whose only job is to decorate the link created byOPMLinkEditPart
with the correct decoration depending on the kind of the link in the model:package com.vainolo.phd.opm.gef.editor.part; import org.eclipse.draw2d.ColorConstants; import org.eclipse.draw2d.PolylineConnection; import org.eclipse.draw2d.PolylineDecoration; import com.vainolo.phd.opm.gef.editor.figure.CircleDecoration; import com.vainolo.phd.opm.model.OPMProceduralLink; import com.vainolo.phd.opm.model.OPMProceduralLinkKind; /** * An extension of a {@link OPMLinkEditPart} used for * {@link OPMProceduralLink} instances. * It add endpoint decorations to the regular link figure. * @author vainolo * */ public class OPMProceduralLinkEditPart extends OPMLinkEditPart { /** * Extend the connection creted by {@link OPMLinkEditPart#createFigure()} by adding * decorations depending on the link kind. * An agent link is decorated at the target with black filled {@link CircleDecoration}. * An instrument link is decorated at the target with a white filled {@link CircleDecoration}. * A consumption or result link is decorated at the target with a {@link PolylineDecoration} * (which is an arrow). * An effect link link is decorated at the source and target with a {@link PolylineDecoration}. * @return a decorated {@link PolylineConnection} figure. */ @Override protected PolylineConnection createFigure() { PolylineConnection connection = super.createFigure(); OPMProceduralLink model = (OPMProceduralLink) getModel(); decorateConnection(connection, model.getKind()); return connection; } /** * Decorate a connection depending on its kind. * @param connection the {@link PolylineConnection} to decorate. * @param kind the {@link OPMProceduralLinkKind} of the model entity. */ private void decorateConnection(PolylineConnection connection, OPMProceduralLinkKind kind) { switch (kind) { case AGENT: CircleDecoration agentDecoration = new CircleDecoration(); agentDecoration.setBackgroundColor(ColorConstants.black); agentDecoration.setFill(true); connection.setTargetDecoration(agentDecoration); break; case INSTRUMENT: CircleDecoration instrumentDecoration = new CircleDecoration(); instrumentDecoration.setBackgroundColor(ColorConstants.white); instrumentDecoration.setFill(true); connection.setTargetDecoration(instrumentDecoration); break; case CONSUMPTION: case RESULT: connection.setTargetDecoration(new PolylineDecoration()); break; case EFFECT: connection.setSourceDecoration(new PolylineDecoration()); connection.setTargetDecoration(new PolylineDecoration()); break; default: throw new IllegalArgumentException("No case for kind "+kind); } } }
- The newly created
EditPart
must be created by theEditPartFactory
when aOPMProceduralLink
model instance is received by the framework, so we must update theOPMEditPartFactory
. We also added a runtime exception when the factory is given a model class that it doesn’t recognize. I added this because while the framework fails when the factory provides itnull
edit parts, this way the problem can be found faster and tracing it is easier.package com.vainolo.phd.opm.gef.editor.part; import com.vainolo.phd.opm.model.OPMLink; import com.vainolo.phd.opm.model.OPMObject; import com.vainolo.phd.opm.model.OPMObjectProcessDiagram; import com.vainolo.phd.opm.model.OPMProceduralLink; import com.vainolo.phd.opm.model.OPMProcess; import org.eclipse.gef.EditPart; import org.eclipse.gef.EditPartFactory; public class OPMEditPartFactory implements EditPartFactory { @Override public EditPart createEditPart(EditPart context, Object model) { EditPart part = null; if (model instanceof OPMObjectProcessDiagram) { part = new OPMObjectProcessDiagramEditPart(); } else if (model instanceof OPMObject) { part = new OPMObjectEditPart(); } else if (model instanceof OPMProcess) { part = new OPMProcessEditPart(); } else if (model instanceof OPMProceduralLink) { // It is important for OPMProceduralLink to be before OPMLink because // they have an is-a relation and we would get the wrong EditPart. part = new OPMProceduralLinkEditPart(); } else if (model instanceof OPMLink) { part = new OPMLinkEditPart(); } else { throw new IllegalArgumentException("Model class "+model.getClass()+" not supported yet."); } if (part != null) { part.setModel(model); } return part; } }
- We must now provide new tools to create the procedural links. This is the place where the links are differentiated – in the
CreationFactory
that produces them (“Ah!” you are probably saying, “so you still had to implement lots of classes”. Well yes, but these are pure utility classes that are written once and forgotten for eternity. Unlike this, a class that exists in the model hierarchy has a large influence on the readability of the model. And 5 classes are even worse). We create one factory for each procedural link kind:package com.vainolo.phd.opm.gef.editor.factory; import org.eclipse.gef.requests.CreationFactory; import com.vainolo.phd.opm.model.OPMFactory; import com.vainolo.phd.opm.model.OPMProceduralLink; import com.vainolo.phd.opm.model.OPMProceduralLinkKind; /** * Factory used by palette tools to create {@link OPMProceduralLink} of * {@link OPMProceduralLinkKind#AGENT} kind. */ public class OPMAgentLinkFactory implements CreationFactory { @Override public Object getNewObject() { OPMProceduralLink link = OPMFactory.eINSTANCE.createOPMProceduralLink(); link.setKind(OPMProceduralLinkKind.AGENT); return link; } @Override public Object getObjectType() { return OPMProceduralLink.class; } }
package com.vainolo.phd.opm.gef.editor.factory; import org.eclipse.gef.requests.CreationFactory; import com.vainolo.phd.opm.model.OPMFactory; import com.vainolo.phd.opm.model.OPMProceduralLink; import com.vainolo.phd.opm.model.OPMProceduralLinkKind; /** * Factory used by palette tools to create {@link OPMProceduralLink} of * {@link OPMProceduralLinkKind#CONSUMPTION} kind. */ public class OPMConsumptionLinkFactory implements CreationFactory { @Override public Object getNewObject() { OPMProceduralLink link = OPMFactory.eINSTANCE.createOPMProceduralLink(); link.setKind(OPMProceduralLinkKind.CONSUMPTION); return link; } @Override public Object getObjectType() { return OPMProceduralLink.class; } }
package com.vainolo.phd.opm.gef.editor.factory; import org.eclipse.gef.requests.CreationFactory; import com.vainolo.phd.opm.model.OPMFactory; import com.vainolo.phd.opm.model.OPMProceduralLink; import com.vainolo.phd.opm.model.OPMProceduralLinkKind; /** * Factory used by palette tools to create {@link OPMProceduralLink} of * {@link OPMProceduralLinkKind#EFFECT} kind. */ public class OPMEffectLinkFactory implements CreationFactory { @Override public Object getNewObject() { OPMProceduralLink link = OPMFactory.eINSTANCE.createOPMProceduralLink(); link.setKind(OPMProceduralLinkKind.EFFECT); return link; } @Override public Object getObjectType() { return OPMProceduralLink.class; } }
package com.vainolo.phd.opm.gef.editor.factory; import org.eclipse.gef.requests.CreationFactory; import com.vainolo.phd.opm.model.OPMFactory; import com.vainolo.phd.opm.model.OPMProceduralLink; import com.vainolo.phd.opm.model.OPMProceduralLinkKind; /** * Factory used by palette tools to create {@link OPMProceduralLink} of * {@link OPMProceduralLinkKind#INSTRUMENT} kind. */ public class OPMInstrumentLinkFactory implements CreationFactory { @Override public Object getNewObject() { OPMProceduralLink link = OPMFactory.eINSTANCE.createOPMProceduralLink(); link.setKind(OPMProceduralLinkKind.INSTRUMENT); return link; } @Override public Object getObjectType() { return OPMProceduralLink.class; } }
package com.vainolo.phd.opm.gef.editor.factory; import org.eclipse.gef.requests.CreationFactory; import com.vainolo.phd.opm.model.OPMFactory; import com.vainolo.phd.opm.model.OPMProceduralLink; import com.vainolo.phd.opm.model.OPMProceduralLinkKind; /** * Factory used by palette tools to create {@link OPMProceduralLink} of * {@link OPMProceduralLinkKind#RESULT} kind. */ public class OPMResultLinkFactory implements CreationFactory { @Override public Object getNewObject() { OPMProceduralLink link = OPMFactory.eINSTANCE.createOPMProceduralLink(); link.setKind(OPMProceduralLinkKind.RESULT); return link; } @Override public Object getObjectType() { return OPMProceduralLink.class; } }
And finally, we create the tools for the links:
package com.vainolo.phd.opm.gef.editor; import org.eclipse.gef.palette.ConnectionCreationToolEntry; import org.eclipse.gef.palette.CreationToolEntry; import org.eclipse.gef.palette.PaletteGroup; import org.eclipse.gef.palette.PaletteRoot; import org.eclipse.gef.palette.SelectionToolEntry; import com.vainolo.phd.opm.gef.editor.factory.OPMAgentLinkFactory; import com.vainolo.phd.opm.gef.editor.factory.OPMConsumptionLinkFactory; import com.vainolo.phd.opm.gef.editor.factory.OPMEffectLinkFactory; import com.vainolo.phd.opm.gef.editor.factory.OPMInstrumentLinkFactory; import com.vainolo.phd.opm.gef.editor.factory.OPMLinkFactory; import com.vainolo.phd.opm.gef.editor.factory.OPMObjectFactory; import com.vainolo.phd.opm.gef.editor.factory.OPMProcessFactory; import com.vainolo.phd.opm.gef.editor.factory.OPMResultLinkFactory; import com.vainolo.phd.opm.gef.editor.tool.CreationAndDirectEditTool; /** * Tool pallete for the {@link OPMGraphicalEditor}. */ public class OPMGraphicalEditorPalette extends PaletteRoot { PaletteGroup group; public OPMGraphicalEditorPalette() { addGroup(); addSelectionTool(); addOPMObjectTool(); addOPMProcessTool(); addOPMLinkTool(); addOPMProceduralLinkTools(); } private void addSelectionTool() { SelectionToolEntry entry = new SelectionToolEntry(); group.add(entry); setDefaultEntry(entry); } private void addGroup() { group = new PaletteGroup("OPM Controls"); add(group); } private void addOPMObjectTool() { CreationToolEntry entry = new CreationToolEntry("OPMObject", "Create a new Object", new OPMObjectFactory(), null, null); entry.setToolClass(CreationAndDirectEditTool.class); group.add(entry); } private void addOPMProcessTool() { CreationToolEntry entry = new CreationToolEntry("OPMProcess", "Create a new Process", new OPMProcessFactory(), null, null); entry.setToolClass(CreationAndDirectEditTool.class); group.add(entry); } private void addOPMLinkTool() { ConnectionCreationToolEntry entry = new ConnectionCreationToolEntry("Link", "Creates a new link", new OPMLinkFactory(), null, null); group.add(entry); } /** * Add tools to create procedural links in the diagram. */ private void addOPMProceduralLinkTools() { ConnectionCreationToolEntry entry; entry = new ConnectionCreationToolEntry("Agent", "Create a new Agent link", new OPMAgentLinkFactory(), null, null); group.add(entry); entry = new ConnectionCreationToolEntry("Instrument", "Create a new Instrument link", new OPMInstrumentLinkFactory(), null, null); group.add(entry); entry = new ConnectionCreationToolEntry("Consumption", "Create a new Consumption link", new OPMConsumptionLinkFactory(), null, null); group.add(entry); entry = new ConnectionCreationToolEntry("Result", "Create a new Result link", new OPMResultLinkFactory(), null, null); group.add(entry); entry = new ConnectionCreationToolEntry("Effect", "Create a new Effect link", new OPMEffectLinkFactory(), null, null); group.add(entry); } }
- That’s it!. Fire your editor and add some procedural links. Check how this looks in my editor:
Finally starting to look like a real editor.
You can find the final project files here.
Next Tutorial: Creating an OPM GEF Editor – Part 14: Refactoring, Refactoring and More Refactoring
The model file you link at the end of step 1 is not the right model for this tutorial. It seems like a much more complicated model. The correct one is included in the project files at the end though.
In the CircleDecoration setReferencePoint method, the code can be simplified a bit like so:
double k = (d-RADIUS)/d;
double longx = p.x-location.x;
double longy = p.y-location.y;
double shortx = k*longx;
double shorty = k*longy;
int rx, ry;
rx = p.x – (int)shortx;
ry = p.y – (int)shorty;
Here longx and longy give us the relative coordinates of r instead of the distances so we don’t need the if statements.
Thanks! I knew there had to be a nicer way to do it. I also fixed the link; Thanks for pointing it out.
In case you are interested here’s what I think is a more object oriented way to support the various types of Procedural Links with an enum.
Instead of defining the Enum in the ECore model I defined a plain old Java Enum as follows. A nice feature of JAVA is that an Enum can have an abstract interface which each enumeration must implement. This way does away with switch statements because the calling logic will call the interface of the Enum for a given function (see OPMProceduralLinkEditPart.createFigure() in Step 2). It also centralizes the implementation for the various cases (enumerations), which I believe is easier to maintain.
// Step 1
// Define a JAVA Enum with the interface needed to support all of the functional behaviours
// exhibited by the various types of procedural links. In this case we just have decorateConnection(…).
package jon.sandbox.model.opm.helper;
import org.eclipse.draw2d.ColorConstants;
import org.eclipse.draw2d.PolylineConnection;
import org.eclipse.draw2d.PolylineDecoration;
public enum OPMProceduralLinkKind2
{
eAgent
{
@Override
public void decorateConnection(PolylineConnection connection)
{
CircleDecoration agentDecoration = new CircleDecoration();
agentDecoration.setBackgroundColor(ColorConstants.black);
agentDecoration.setFill(true);
connection.setTargetDecoration(agentDecoration);
}
},
eInstrument
{
@Override
public void decorateConnection(PolylineConnection connection)
{
CircleDecoration instrumentDecoration = new CircleDecoration();
instrumentDecoration.setBackgroundColor(ColorConstants.white);
instrumentDecoration.setFill(true);
connection.setTargetDecoration(instrumentDecoration);
}
},
eConsumption
{
@Override
public void decorateConnection(PolylineConnection connection)
{
connection.setTargetDecoration(new PolylineDecoration());
}
}
,
eResult
{
@Override
public void decorateConnection(PolylineConnection connection)
{
connection.setTargetDecoration(new PolylineDecoration());
}
},
eEffect
{
@Override
public void decorateConnection(PolylineConnection connection)
{
connection.setSourceDecoration(new PolylineDecoration());
connection.setTargetDecoration(new PolylineDecoration());
}
};
public abstract void decorateConnection(PolylineConnection connection);
}
// Step 2
// a. Modify the ECore model to have an EDataType that wraps OPMProceduralLinkKind2
// b. Update OPMProceduralLink to use OPMProceduralLinkKind2 for the “kind”
// c. Re-generate the model and fix the various compilation errors
public class OPMProceduralLinkEditPart extends OPMLinkEditPart
{
@Override
protected PolylineConnection createFigure()
{
PolylineConnection connection = super.createFigure();
OPMProceduralLink model = (OPMProceduralLink) getModel();
//decorateConnection(connection, model.getKind());
model.getKind().decorateConnection(connection); // How cool is this!?
return connection;
}
public class OPMInstrumentLinkFactory implements CreationFactory
{
@Override
public Object getNewObject()
{
OPMProceduralLink link = OpmFactory.eINSTANCE.createOPMProceduralLink();
// link.setKind(OPMProceduralLinkKind.INSTRUMENT);
link.setKind(OPMProceduralLinkKind2.eInstrument);
return link;
}
…
}
….
// Step 3
// In OpmFactoryImpl implement the create/convert methods for OPMProceduralLinkKind2
/**
*
*
* @generated NOT
*/
public jon.sandbox.model.opm.helper.OPMProceduralLinkKind2 createOPMProceduralLinkKind2FromString(
EDataType eDataType, String initialValue)
{
OPMProceduralLinkKind2 rtn = OPMProceduralLinkKind2.valueOf(initialValue);
return rtn;
}
/**
*
*
* @generated NOT
*/
public String convertOPMProceduralLinkKind2ToString(EDataType eDataType, Object instanceValue)
{
String str = super.convertToString(eDataType, instanceValue);
return str;
}
For a long time I wanted to learn GEF and now that I have some free time to do, I have found your tutorial incredibly helpful. Thank you very much for creating this tutorial and all of the hard work you put into it.
Hi Jon. Nice you enjoyed the tutorial. And thanks for the implementation – it’s good to learn new things every day.
Hey,
I tried implementing decorated circle as a node. It doesn’t works. In many forums, people say that it works only at the end of a polyline. Is this true ? Is there a better way to do this ?
AFAIK a decoration cannot be a
Node
and must be aFigure