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 angles
  • FanRouter: when two connections are going to overlap, it adds a bendpoint to one in a fan-like fashion
  • BendpointConnectionRouter: 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.