Creating a GEF Editor – Part 9: Connections

Previous Tutorial: Creating a GEF Editor – Part 8: Delete, Undo and Redo

Having a functional editor on which we can add figures is already pretty neat, but we are still missing one very important element in our editor: connections. Connections are generally used to describe some kind of relation between different entities in the diagram, and are shown as lines that connect two different figures. In this tutorial you will learn how to add basic GEF connections, and if it doesn’t get too long, we’ll also add to them some extra functionality. This is going to be fun! Enjoy.

  1. Knowing that we wanted to create connections in our editor, our model already contains an EClass that represents these connections, called OPMLink. An OPMLink simply points to a source and target OPMThing in the model (it also contains a list of bendpoints that we will use to draw our connection, but more on this later). So we don’t have to do anything to implement connections in our model. Whoppi!
  2. Now we’ll add to the editor a new EditPart called OPMLinkEditPart. An EditPart that is used as a connection implements the ConnectionEditPartinterface (either directly or by subclassing the AbstractConnectionEditPart class as we do), which requires the class to provide functionality to get/set source and target EditPart instances. Our first connection implementation is going to be very simple, having only a line that connects between to two diagram entities:
    package com.vainolo.phd.opm.gef.editor.part;
    
    import org.eclipse.draw2d.IFigure;
    import org.eclipse.draw2d.PolylineConnection;
    import org.eclipse.gef.EditPolicy;
    import org.eclipse.gef.editparts.AbstractConnectionEditPart;
    import org.eclipse.gef.editpolicies.ConnectionEndpointEditPolicy;
    
    public class OPMLinkEditPart extends AbstractConnectionEditPart {
    
    	public OPMLinkEditPart() {
    		super();
    	}
    	
    	@Override protected void createEditPolicies() {
    		installEditPolicy(EditPolicy.CONNECTION_ENDPOINTS_ROLE, new ConnectionEndpointEditPolicy());
    	}
    
    	@Override protected IFigure createFigure() {
    		PolylineConnection conn = new PolylineConnection();
    		return conn; 
    	}
    
    }
    
  3. Now we start all of the setup required for the connections. First we are going to add connection anchors to our OPMThing figures. Connection anchors define where the connection is connected to its endpoint (source or target EditPart). We use both ChopBoxAnchor and EllipseAnchor, which simply sets the anchor in the intersection point between the connection and the edge of the figure (this is better explained in pictures):
    • ChopBoxAnchor: will be used for OPMObject.
    • EllipseAnchor: will be used for OPMProcess.

    To add the anchors, we modify the OPMThingFigure interface adding it a getConnectionAnchor method:

    package com.vainolo.phd.opm.gef.editor.figure;
    
    import org.eclipse.draw2d.ConnectionAnchor;
    import org.eclipse.draw2d.IFigure;
    import org.eclipse.draw2d.Label;
    
    public interface OPMThingFigure extends IFigure {
    	public Label getNameLabel();
    	public ConnectionAnchor getConnectionAnchor();	
    }
    

    And we implement in both in the OPMObjectFigure and OPMProcessFigure:

    package com.vainolo.phd.opm.gef.editor.figure;
    
    import org.eclipse.draw2d.ChopboxAnchor;
    import org.eclipse.draw2d.ConnectionAnchor;
    import org.eclipse.draw2d.Figure;
    import org.eclipse.draw2d.Graphics;
    import org.eclipse.draw2d.Label;
    import org.eclipse.draw2d.RectangleFigure;
    import org.eclipse.draw2d.XYLayout;
    import org.eclipse.draw2d.geometry.Rectangle;
    
    public class OPMObjectFigure extends Figure implements OPMThingFigure {
    	private Label nameLabel;
    	private RectangleFigure rectangle;
    	private ConnectionAnchor connectionAnchor;
    	
    	public OPMObjectFigure() {
    		setLayoutManager(new XYLayout());
    		rectangle = new RectangleFigure();
    		add(rectangle);
    		nameLabel = new Label();
    		add(nameLabel);
    	}
    
    	@Override protected void paintFigure(Graphics graphics) {
    		Rectangle r = getBounds().getCopy();
    		setConstraint(rectangle, new Rectangle(0, 0, r.width, r.height));
    		setConstraint(nameLabel, new Rectangle(0, 0, r.width, r.height));
    		nameLabel.invalidate();		
    		rectangle.invalidate();
    	}
    	
    	public Label getNameLabel() {
    		return nameLabel;
    	}
    	
    	public ConnectionAnchor getConnectionAnchor() {
    		if (connectionAnchor == null) {
    			connectionAnchor = new ChopboxAnchor(this);
    		}
    		return connectionAnchor;
    	}
    }
    
    package com.vainolo.phd.opm.gef.editor.figure;
    
    import org.eclipse.draw2d.ConnectionAnchor;
    import org.eclipse.draw2d.Ellipse;
    import org.eclipse.draw2d.EllipseAnchor;
    import org.eclipse.draw2d.Figure;
    import org.eclipse.draw2d.Graphics;
    import org.eclipse.draw2d.Label;
    import org.eclipse.draw2d.XYLayout;
    import org.eclipse.draw2d.geometry.Rectangle;
    
    public class OPMProcessFigure extends Figure implements OPMThingFigure {
    	private Label nameLabel;
    	private Ellipse ellipse;
    	private ConnectionAnchor connectionAnchor;	
    	
    	public OPMProcessFigure() {
    		setLayoutManager(new XYLayout());
    		ellipse = new Ellipse();
    		add(ellipse);
    		nameLabel = new Label();
    		add(nameLabel);
    	}
    
    	@Override protected void paintFigure(Graphics graphics) {
    		Rectangle r = getBounds().getCopy();
    		setConstraint(ellipse, new Rectangle(0, 0, r.width, r.height));
    		setConstraint(nameLabel, new Rectangle(0, 0, r.width, r.height));
    		ellipse.invalidate();
    		nameLabel.invalidate();
    	}
    	
    	public Label getNameLabel() {
    		return nameLabel;
    	}
    	
    	public ConnectionAnchor getConnectionAnchor() {
    		if (connectionAnchor == null) {
    			connectionAnchor = new EllipseAnchor(this);
    		}
    		return connectionAnchor;
    	}		
    }
    
  4. The connection anchors we just created are used by the OPMThingEditPart when adding new connections to the diagram. I should show you this code now, but the thing is that I like to display full files (no “…” in the middle) and also to do things so that the code compiles at all times. So please bear with me for a moment. Since we will be adding new connections in the diagram, and diagram operations are done using a Command and an EditPolicy, we have to implement one of each. Implementing the Command is fairly simple:
    package com.vainolo.phd.opm.gef.editor.command;
    
    import org.eclipse.gef.commands.Command;
    
    import com.vainolo.phd.opm.model.OPMLink;
    import com.vainolo.phd.opm.model.OPMObjectProcessDiagram;
    import com.vainolo.phd.opm.model.OPMThing;
    
    public class OPMLinkCreateCommand extends Command {
    	
    	private OPMThing source;
    	private OPMThing target;
    	private OPMLink link;
    	private OPMObjectProcessDiagram opd;
    
    	@Override 
    	public boolean canExecute() {
    		return source != null && target != null && link != null;
    	}
    	
    	@Override public void execute() {
    		link.setSource(source);
    		link.setTarget(target);
    		link.setOpd(opd);
    	}
    
    	@Override public void undo() {
    		link.getSource().getOutgoingLinks().remove(link);
    		link.setSource(null);
    		link.getTarget().getIncomingLinks().remove(link);
    		link.setTarget(null);
    		link.setOpd(null);
    	}
    
    	public void setTarget(OPMThing target) {
    		this.target = target;
    	}
    	
    	public void setSource(OPMThing source) {
    		this.source = source;
    	}
    	
    	public void setLink(OPMLink link) {
    		this.link = link;
    	}
    	
    	public void setOPD(OPMObjectProcessDiagram opd) {
    		this.opd = opd;
    	}
    }
    

    The EditPolicy that creates this command is a subclass of GraphicalNodeEditPolicy, a policy used for creating and reconnecting connections. The Command is created when the user selects the source of the connection (by clicking on it after selecting a connection creation tool – which we will implement in a few more steps) and updated when the user select the target of the connection. In the middle of the process the Command cannot be executed because of the code we inserted in the canExecute method. I’m not sure if the framework tries to execute it right after the source is selected, but it seems that this can happen. It also seems possible to execute two different commands to create a link, one when the source is select and one when the target is selected, and I will investigate this in the future. But for now, this is the GraphicalNodeEditPolicy that we’ll use:

    package com.vainolo.phd.opm.gef.editor.policy;
    
    import org.eclipse.gef.commands.Command;
    import org.eclipse.gef.editpolicies.GraphicalNodeEditPolicy;
    import org.eclipse.gef.requests.CreateConnectionRequest;
    import org.eclipse.gef.requests.ReconnectRequest;
    
    import com.vainolo.phd.opm.gef.editor.command.OPMLinkCreateCommand;
    import com.vainolo.phd.opm.model.OPMLink;
    import com.vainolo.phd.opm.model.OPMThing;
    
    public class OPMThingGraphicalNodeEditPolicy extends GraphicalNodeEditPolicy {
    
    	@Override protected Command getConnectionCompleteCommand(CreateConnectionRequest request) {
    		OPMLinkCreateCommand result = (OPMLinkCreateCommand) request.getStartCommand();
    		result.setTarget((OPMThing)getHost().getModel());
    		return result;
    	}
    
    	@Override protected Command getConnectionCreateCommand(CreateConnectionRequest request) {
    		OPMLinkCreateCommand result = new OPMLinkCreateCommand();
    		result.setSource((OPMThing)getHost().getModel());
    		result.setLink((OPMLink) request.getNewObject());
    		result.setOPD(((OPMThing)getHost().getModel()).getOpd());
    		request.setStartCommand(result);
    		return result;
    	}
    
    	@Override protected Command getReconnectTargetCommand(ReconnectRequest request) {
    		return null;
    	}
    
    	@Override protected Command getReconnectSourceCommand(ReconnectRequest request) {
    		return null;
    	}
    }
    
  5. OK, we can now make all the changes to the OPMThingEditPart. First, we make it implement the NodeEditPart interface, which is used by other GEF classes (like AbstractConnectionEditPart) to better manage the connections in the diagram. This interface requires that the class provide ConnectionAnchor instances for the ConnectionEditPart instances which are connected to it, and also for connections that are currently being created (using Request instances). Second, we add the newly created OPMThingGraphicalNodeEditPolicy to the OPMThingEditPart. And last, we provide the GEF framework with a way to know which connections there are in the diagram by implementing the getModelSourceConnections and getModelTargetConnections methods, just like when we implemented the getModelChildren method in the OPMObjectProcessDiagramEditPart to make it know which model elements existed in our diagram. The GEF framework calls these methods and for each unique OPMLink instance that is found it creates a new OPMLinkEditPart and adds it to the diagram:
    package com.vainolo.phd.opm.gef.editor.part;
    
    import java.util.List;
    
    import org.eclipse.draw2d.ConnectionAnchor;
    import org.eclipse.draw2d.Label;
    import org.eclipse.emf.common.notify.Adapter;
    import org.eclipse.emf.common.notify.Notification;
    import org.eclipse.emf.common.notify.Notifier;
    import org.eclipse.gef.ConnectionEditPart;
    import org.eclipse.gef.EditPolicy;
    import org.eclipse.gef.NodeEditPart;
    import org.eclipse.gef.Request;
    import org.eclipse.gef.RequestConstants;
    import org.eclipse.gef.editparts.AbstractGraphicalEditPart;
    import org.eclipse.jface.viewers.TextCellEditor;
    
    import com.vainolo.phd.opm.gef.editor.figure.OPMThingFigure;
    import com.vainolo.phd.opm.gef.editor.policy.OPMThingComponentEditPolicy;
    import com.vainolo.phd.opm.gef.editor.policy.OPMThingDirectEditPolicy;
    import com.vainolo.phd.opm.gef.editor.policy.OPMThingGraphicalNodeEditPolicy;
    import com.vainolo.phd.opm.model.OPMLink;
    import com.vainolo.phd.opm.model.OPMThing;
    
    public abstract class OPMThingEditPart extends AbstractGraphicalEditPart implements NodeEditPart {
    
    	private OPMThingAdapter adapter;
    	
    	public OPMThingEditPart() {
    		super();
    		adapter = new OPMThingAdapter();
    	}
    	
    	@Override protected void createEditPolicies() {
    		installEditPolicy(EditPolicy.DIRECT_EDIT_ROLE, new OPMThingDirectEditPolicy());
    		installEditPolicy(EditPolicy.GRAPHICAL_NODE_ROLE, new OPMThingGraphicalNodeEditPolicy());
    		installEditPolicy(EditPolicy.COMPONENT_ROLE, new OPMThingComponentEditPolicy());
    	}
    
    	@Override protected List<OPMLink> getModelSourceConnections() {
    		OPMThing model = (OPMThing)getModel();
    		return model.getOutgoingLinks();
    	}
    
    	@Override protected List<OPMLink> getModelTargetConnections() {
    		OPMThing model = (OPMThing)getModel();
    		return model.getIncomingLinks();
    	}
    
    	@Override protected void refreshVisuals() {
    		OPMThingFigure figure = (OPMThingFigure)getFigure();
    		OPMThing model = (OPMThing)getModel();
    		OPMObjectProcessDiagramEditPart parent = (OPMObjectProcessDiagramEditPart) getParent();
    		
    		figure.getNameLabel().setText(model.getName());
    		parent.setLayoutConstraint(this, figure, model.getConstraints());
    	}
    
    	public ConnectionAnchor getSourceConnectionAnchor(ConnectionEditPart connection) {
    		return ((OPMThingFigure)getFigure()).getConnectionAnchor();
    	}
    
    	public ConnectionAnchor getTargetConnectionAnchor(ConnectionEditPart connection) {
    		return ((OPMThingFigure)getFigure()).getConnectionAnchor();
    	}
    
    	@Override public ConnectionAnchor getSourceConnectionAnchor(Request request) {
    		return ((OPMThingFigure)getFigure()).getConnectionAnchor();
    	}
    
    	@Override public ConnectionAnchor getTargetConnectionAnchor(Request request) {
    		return ((OPMThingFigure)getFigure()).getConnectionAnchor();
    	}	
    	
    	@Override public void activate() {
    		if(!isActive()) {
    			((OPMThing)getModel()).eAdapters().add(adapter);
    		}
    		super.activate();
    	}
    
    	@Override public void deactivate() {
    		if(isActive()) {
    			((OPMThing)getModel()).eAdapters().remove(adapter);
    		}
    
    		super.deactivate();
    	}
    	
    	@Override public void performRequest(Request req) {
    		if(req.getType() == RequestConstants.REQ_DIRECT_EDIT) {
    			performDirectEditing();
    		}
    	}
    	
    	private void performDirectEditing() {
    		Label label = ((OPMThingFigure)getFigure()).getNameLabel();
    		OPMThingDirectEditManager manager = new OPMThingDirectEditManager(this, TextCellEditor.class, new OPMThingCellEditorLocator(label), label);
    		manager.show();
    	}	
    	
    	public class OPMThingAdapter implements Adapter {
    
    		// Adapter interface
    		@Override public void notifyChanged(Notification notification) {
    			refreshVisuals();
    			refreshSourceConnections();
    			refreshTargetConnections();
    		}
    
    		@Override public Notifier getTarget() {
    			return (OPMThing)getModel();
    		}
    
    		@Override public void setTarget(Notifier newTarget) {
    			// Do nothing.
    		}
    
    		@Override public boolean isAdapterForType(Object type) {
    			return type.equals(OPMThing.class);
    		}
    	}
    }
    
  6. To finish the job, we must implement a factory that creates OPMLink instances and add a tool to the OPMGraphicalEditorPalette to handle the creation of new connections. First comes the factory:
    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.OPMLink;
    
    public class OPMLinkFactory implements CreationFactory {
    
    	@Override public Object getNewObject() {
    		return OPMFactory.eINSTANCE.createOPMLink();
    	}
    
    	@Override public Object getObjectType() {
    		return OPMLink.class;
    	}
    
    }
    

    And second comes the tool:

    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.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.tool.CreationAndDirectEditTool;
    
    public class OPMGraphicalEditorPalette extends PaletteRoot {
    
    	PaletteGroup group;
    	
    	public OPMGraphicalEditorPalette() {
    		addGroup();
    		addSelectionTool();
    		addOPMObjectTool();
    		addOPMProcessTool();
    		addOPMLinkTool();
    	}
    	
    	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);
    	}	
    }
    
  7. One last thing (and thanks to Ouri for the comment), we have to add the OPMLink model to the OPMEditPartFactory so the edit parts are created by the framework when needed
    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.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 OPMLink) {
                part = new OPMLinkEditPart();
            }
    			
            if(part != null) {
                part.setModel(model);
            }
    
            return part;
        }
    }
    
  8. Fire up your editor and add one or two links to it. Looks good, doesn’t it? But don’t get too exited because we still haven’t implemented connection removal so every link you add will stay until we do this… hopefully soon. We must also fix the command that removes things because it must also remove the connections… but it’s late and I think we’ve had enough for today.

You can find the final project files here.

I really hope you enjoyed this tutorial. So long, and thanks for all the fish :-).

Next Tutorial: Creating a GEF Editor – Part 10: Deleting Connections and Fixing of Thing Delete Command