Building a GEF-based Eclipse editor - part 1
Par Julien, mardi 9 août 2005 à 09:58 :: Eclipse :: #161 :: rss
After an introduction to this serie on building a GEF editor plug-in for Eclipse, it's time to actually see some lines of code
In this entry, we are going to see how to implement the visual part of the editor, namely the figures. To do that, we are going to use the Draw2D API and see the results in a standalone SWT application.
Recalling that GEF is a generic MVC framework, we are aiming for today at building the 'V' part. To do that, we can already start and create a plug-in project for our editor. I won't go much into details here since any Eclipse plug-ins developer should be familiar with this, but if you are a beginner, just create a plug-in with the default option and do not use any template (yes, including the editor template). Once you are done, open the plug-in descriptor and manifest editor under the dependencies tab. Here add a dependency toward the following plug-ins:
org.eclipse.gef(we don't already need it for the moment but it doesn't hurt)org.eclipse.draw2d.
With this, your project should resolve to the good classpath. You now need a package to hold your view classes that we call figures in the Draw2D/GEF terminology. I suggest a very laconic naming scheme, for instance mine is fr.isima.ponge.wsprotocol.gefeditor.figures. Before going further, we need a few explanations. Figures are the most basic elements that can be handled: there is a IFigure interface and a concrete base implementation called Figure. Starting from this pretty anything else is made of subclasses, including:
- various shapes (rectangles, rounded rectangles, ellipses, circles, ...)
- some kinds of widgets (labels, buttons, checkboxes, ...)
- connections to link between figures.
The connections are most of time instances of PolylineConnection which allows for adding bendpoints. Connections also have support for decorations (for instance an arrow) and decorations (for instance text labels). Connections then need a router. A router is reponsible for a routing policy. Here are some sample routers:
ManhattanConnectionRouter: straight lines with 90 degrees anglesFanRouter: when two connections are going to overlap, it adds a bendpoint to one in a fan-like fashionBendpointConnectionRouter: manages freeform bendpoints.
Connecting figures requires the knowledge of some anchor points. An anchor is an object responsible for computing the references points for the connection on the figures beeing connected. A simple one is ChopboxAnchor which computes the bounding box of the figure then gives the center of the box as a reference point. With this anchor, the connections are targetting the center of the figures. It is of course possible to define your own anchors if you have special requirements like pixel-precise connection points.
For the need of my editor, I needed to represent the states with rounded boxes. The color depends of the kind of state (initial, normal or final). The border line is a little bit darker than the one used to paint the figure. When a state is final, I need to draw a double border. Finally, I wanted to draw a simple shadow which is just a light gray rounded box. The Draw2D API allows for making more complex kind of drawings with shades, but this is not the point for the moment. I also want to display some text on thge figure.
First, let's declare our states figure with some attributes (comments removed and compact formatting):

public class StateFigure extends Figure
{
protected Label label = new Label();
protected Color fgColor;
protected Color bgColor;
protected boolean singleBorder = true;
protected static Color initialFgColor;
protected static Color initialBgColor;
protected static Color normalFgColor;
protected static Color normalBgColor;
protected static Color finalFgColor;
protected static Color finalBgColor;
protected static Color shadowColor;
protected String text;
protected boolean initialState = false;
protected boolean finalState = false;
(...)
The colors are going to be shared accross instances. Please note that the label is not from the SWT packages but rather from the Draw2D ones. We are going to put the label as a child of the figure with a BorderLayout. Here is what the constructor looks like:
public StateFigure(String text, boolean initialState, boolean finalState)
{
super();
// Add the label
BorderLayout layout = new BorderLayout();
layout.setHorizontalSpacing(4);
layout.setVerticalSpacing(4);
setLayoutManager(layout);
label.setForegroundColor(ColorConstants.black);
add(label, BorderLayout.CENTER);
// Lazily instanciate the shared colors
if (initialFgColor == null)
{
Device device = Display.getCurrent();
initialFgColor = new Color(device, 170, 170, 170);
initialBgColor = new Color(device, 250, 250, 250);
normalFgColor = new Color(device, 175, 175, 149);
normalBgColor = new Color(device, 255, 255, 229);
finalFgColor = new Color(device, 149, 175, 151);
finalBgColor = new Color(device, 229, 255, 231);
shadowColor = new Color(device, 230, 230, 230);
}
// Init
setText(text);
setInitialState(initialState);
setFinalState(finalState);
}
The next step is to add getters/setters for the text, initialState and finalState attributes. Uppon each setter invocation, we are going to invoke a method to update the visual attributes:
private void updateContext()
{
// Border
singleBorder = !isFinalState();
// Colors
if (isInitialState())
{
fgColor = initialFgColor;
bgColor = initialBgColor;
}
else if (isFinalState())
{
fgColor = finalFgColor;
bgColor = finalBgColor;
}
else
{
fgColor = normalFgColor;
bgColor = normalBgColor;
}
// Text
label.setText(getText());
}
Let's now make some painting (refer to the API documentations for more details - everything is really straightforward):
protected void paintFigure(Graphics graphics)
{
// Inits
super.paintFigure(graphics);
Rectangle bounds = getBounds().getCopy().resize(-9, -9).translate(4, 4);
final int round = 25;
final int sround = 30;
// Shadow / experimental
graphics.setBackgroundColor(shadowColor);
graphics.fillRoundRectangle(bounds.getTranslated(4, 4), sround, sround);
// Drawings
graphics.setForegroundColor(fgColor);
graphics.setBackgroundColor(bgColor);
graphics.fillRoundRectangle(bounds, round, round);
graphics.drawRoundRectangle(bounds, round, round);
if (!singleBorder)
{
bounds.expand(-4, -4);
graphics.drawRoundRectangle(bounds, round, round);
}
// Cleanups
graphics.restoreState();
}
Finally, let's provide an anchor for requesters, taking care of the apparent figure bounding (and not the real one !):
public ConnectionAnchor getAnchor()
{
return new ChopboxAnchor(this) {
protected Rectangle getBox()
{
Rectangle base = super.getBox();
return base.getResized(-4, -4).getTranslated(4, 4);
}
};
}
That's it, we now have our states figure
Im my model, states are connected by operations. Let's subclass PolylineConnection to provide a specialized connection. In particular, I need different colors depending on the operation kind and there is even one kind which implies having dashed lines. I also need to be able to put some text. Again, the Draw2D API makes things straightforward:

public class OperationFigure extends PolylineConnection (...) // A good bunch of attributes that don't really matter
The constructor:
public OperationFigure(int type, String text)
{
super();
// UI
targetDecoration = getTargetDecoration();
setTargetDecoration(targetDecoration);
setLineWidth(2);
label = new Label();
label.setOpaque(true);
MidpointLocator locator = new MidpointLocator(this, 0);
add(label, locator);
// Colors
if (inputColor == null)
{
inputColor = ColorConstants.blue;
outputColor = ColorConstants.red;
implicitColor = ColorConstants.gray;
}
// Data
setType(type);
setText(text);
}
The target decoration is a bigger arrow than the default one:
protected RotatableDecoration getTargetDecoration()
{
if (targetDecoration == null)
{
PointList points = new PointList();
points.addPoint(-2, 2);
points.addPoint(0, 0);
points.addPoint(-2, -2);
targetDecoration = new PolygonDecoration();
((PolygonDecoration) targetDecoration).setTemplate(points);
}
return targetDecoration;
}
Important, the attributes setters:
public void setText(String text)
{
this.text = text;
label.setText(text);
}
public void setType(int type)
{
if (type < INPUT || type > IMPLICIT)
{
return;
}
this.type = type;
switch (type)
{
case INPUT:
setForegroundColor(inputColor);
setLineStyle(Graphics.LINE_SOLID);
break;
case OUTPUT:
setForegroundColor(outputColor);
setLineStyle(Graphics.LINE_SOLID);
break;
case IMPLICIT:
setForegroundColor(implicitColor);
setLineStyle(Graphics.LINE_DASH);
break;
default:
break;
}
}
... and that's it for this figure. Simple isn't it ?
We are now ready for visually testing the rendering of the figures. To do that, we are going to create a very simple SWT application. The code is heavily inspired from the Connections and anchors demo from the Draw2D documentation, so I am only going to pick some extracts of my own code. Please refer to the Draw2D documentation here. Let's use a simple FanRouter for the connections.
public class FiguresPOC
{
public static void main(String args)
{
// Window + display context
Shell shell = new Shell();
shell.setSize(640, 480);
shell.open();
shell.setText("Figures POC"); //$NON-NLS-1$
LightweightSystem lws = new LightweightSystem(shell);
IFigure panel = new Figure();
panel.setOpaque(true);
panel.setBackgroundColor(ColorConstants.white);
lws.setContents(panel);
// State 1
StateFigure s1 = new StateFigure("State #1", true, false); //$NON-NLS-1$
s1.setBounds(new Rectangle(10, 10, 100, 60));
panel.add(s1);
new Dragger(s1);
// State 2
StateFigure s2 = new StateFigure("State #2", false, false); //$NON-NLS-1$
s2.setBounds(s1.getBounds().getTranslated(new Point(200, 100)));
panel.add(s2);
new Dragger(s2);
(...)
// Router
FanRouter router = new FanRouter();
router.setSeparation(40);
// T1
OperationFigure t1 = new OperationFigure(OperationFigure.INPUT, "T1"); //$NON-NLS-1$
t1.setSourceAnchor(s1.getAnchor());
t1.setTargetAnchor(s2.getAnchor());
t1.setConnectionRouter(router);
panel.add(t1);
// Run
Display display = Display.getDefault();
while (!shell.isDisposed())
{
if (!display.readAndDispatch())
display.sleep();
}
}
static class Dragger extends MouseMotionListener.Stub implements MouseListener
(...) // same as the API documentation
With this, we should get a window where the states can be dragged around. To run the program, use the Run as SWT application command of Eclipse. Here is how it looks like for me:

You should now have understood the basic ideas behind the Draw2D API. I suggest that you define your figures and test them in a sample SWT application. I strongly discourage you of developping the figures inside your editor while you develop it. This path is much easier and reduces the launching times.
Stay tuned for the next entry where we will start working on the actual editor part
Since then, feel free to comment here if some things are not clear of you have a suggestion/better idea.


Commentaires
1. Le mercredi 17 août 2005 à 19:34, par zeisig
Ajouter un commentaire
Les commentaires pour ce billet sont fermés.