Sunday, January 17, 2010

Adding GEF Connections

Of course this means for us we are doing yet another extension to TutoGEF. Our goal is to visualize various interactions between our Services in our Enterprise. The Enterprise is into engineering and has therefor multiple worksteps in its project management. We will add 3 kinds of GEF Connections:
  • deliver design connection
  • deliver resources connection
  • distribute work packages connection
Those will be displayed on the screen as an arrow.  First we will create the Connection class in our model package ...


Let us create the Connection class. It should look somewhat like the following:



package tutogef.model;

public class Connection {
    
    public static final int CONNECTION_DESIGN = 1;
    public static final int CONNECTION_RESOURCES = 2;
    public static final int CONNECTION_WORKPACKAGES = 3;
    
    private int connectionType;
    
    protected Node sourceNode;
    protected Node targetNode;
    
    public Connection(Node sourceNode, Node targetNode, int connectionType) {
        this.sourceNode = sourceNode;
        this.targetNode = targetNode;
        this.connectionType = connectionType;
    }

    public Node getSourceNode() {
        return sourceNode;
    }
    
    public Node getTargetNode() {
        return targetNode;
    }

    public void connect() {
        sourceNode.addConnections(this);
        targetNode.addConnections(this);
    }
    
    public void disconnect() {
        sourceNode.removeConnection(this);
        targetNode.removeConnection(this);
    }
    
    public void reconnect(Node sourceNode, Node targetNode) {
        if (sourceNode == null || targetNode == null || sourceNode == targetNode) {
            throw new IllegalArgumentException();
        }
        disconnect();
        this.sourceNode = sourceNode;
        this.targetNode = targetNode;
        connect();
    }

    public void setConnectionType(int connectionType) {
        this.connectionType = connectionType;
    }

    public int getConnectionType() {
        return connectionType;
    }
}




In tutogef.model.Node we have to add the ability to hold connections by adding two Lists and two more events for our PropertyChangeListener to act up on:

public class Node implements IAdaptable { 
    // ...
    private List sourceConnections;
    private List targetConnections;
    // ...
    public static final String SOURCE_CONNECTION = "SourceConnectionAdded";
    public static final String TARGET_CONNECTION = "TargetConnectionAdded";
    // ...
    public Node {
        //...
        this.sourceConnections = new ArrayList();
        this.targetConnections = new ArrayList();

        //...
    }

and in order to add or remove connections:
      // ...

    public boolean addConnections (Connection conn) {
        if (conn.getSourceNode() == this) {
            if (!sourceConnections.contains(conn)) {
                if (sourceConnections.add(conn)) {
                    getListeners().firePropertyChange(SOURCE_CONNECTION, null, conn);
                    return true;   
                }
                return false;
            }
        }
        else if (conn.getTargetNode() == this) {
            if (!targetConnections.contains(conn)) {
                if (targetConnections.add(conn)) {
                    getListeners().firePropertyChange(TARGET_CONNECTION, null, conn);
                    return true;
                }
                return false;
            }
        }
        return false;
    }
   
    public boolean removeConnection(Connection conn) {
        if (conn.getSourceNode() == this) {
            if (sourceConnections.contains(conn)) {
                if (sourceConnections.remove(conn)) {
                    getListeners().firePropertyChange(SOURCE_CONNECTION, null, conn);
                    return true;
                }
                return false;
            }
        }
        else if (conn.getTargetNode() == this) {
            if (targetConnections.contains(conn)) {
                if (targetConnections.remove(conn)) {
                    getListeners().firePropertyChange(TARGET_CONNECTION, null, conn);
                    return true;
                }
                return false;
            }
        }   
        return false;
    }


// ... 


    public List getSourceConnectionsArray() {
        return this.sourceConnections;
    }
   
    public List getTargetConnectionsArray() {
        return this.targetConnections;
    }

}



Next we will create the corresponding ConnectionPart to our Connection model. As for all Parts before, we will start with an abstract class called AppAbstractConnectionEditPart in tutogef.part:


package tutogef.part;

import org.eclipse.draw2d.IFigure;
import org.eclipse.gef.editparts.AbstractConnectionEditPart;

public abstract class AppAbstractConnectionEditPart extends AbstractConnectionEditPart  {
    @Override
    protected IFigure createFigure() {
        // TODO Auto-generated method stub
        return super.createFigure();
    }
   
    public void activate() {
        super.activate();
    }
   
    public void deactivate() {
        super.deactivate();
    }

    @Override
    protected void createEditPolicies() {}
}

Now in ConnectionPart which extends the class we just created we also create the figure which is displayed in the Editor later on. Note that there is no need to create a separate ConnectionFigure. That has already been done for us by the draw2d package. Also we set different styles and labels the different connection types.

package tutogef.part;

import org.eclipse.draw2d.ColorConstants;
import org.eclipse.draw2d.IFigure;
import org.eclipse.draw2d.Label;
import org.eclipse.draw2d.MidpointLocator;
import org.eclipse.draw2d.PolygonDecoration;
import org.eclipse.draw2d.PolylineConnection;
import org.eclipse.gef.EditPolicy;
import org.eclipse.gef.editpolicies.ConnectionEndpointEditPolicy;
import org.eclipse.swt.SWT;

import tutogef.model.Connection;
import tutogef.editpolicy.AppConnectionDeleteEditPolicy;

public class ConnectionPart extends AppAbstractConnectionEditPart {
   
    protected IFigure createFigure() {
        PolylineConnection connection = (PolylineConnection) super.createFigure();
        connection.setLineWidth(2);
        PolygonDecoration decoration = new PolygonDecoration();
        decoration.setTemplate(PolygonDecoration.TRIANGLE_TIP);
        connection.setTargetDecoration(decoration);
        Label label = new Label();
        switch (((Connection) getModel()).getConnectionType()) {
            case 1:
                label.setText("deliver design");
                connection.setLineStyle(SWT.LINE_DASH);
                label.setBackgroundColor(ColorConstants.green);
                break;
            case 2:
                label.setText("deliver resources");
                connection.setLineStyle(SWT.LINE_DOT);
                break;
            case 3:
                label.setText("distribute work packages");
                connection.setLineStyle(SWT.LINE_SOLID);
                label.setBackgroundColor(ColorConstants.green);
                break;
            default: return null;
        }
        label.setOpaque( true );
        connection.add(label, new MidpointLocator(connection, 0));
       
        return connection;
    }
   
    @Override
    protected void createEditPolicies() {
        installEditPolicy(EditPolicy.CONNECTION_ROLE, new AppConnectionDeleteEditPolicy());
        installEditPolicy(EditPolicy.CONNECTION_ENDPOINTS_ROLE, new ConnectionEndpointEditPolicy());
    }
}

As we can see there are two new EditPolicies alone in this class. In ServicePart is another one coming up. Lets create them in tutogef.edpolicy and start with AppConnectionDeleteEditPolicy. It is almost self explanatory. It gives our Connection the ability to be removed and invokes the ConnectionDeleteCommand to cleanly do so:


package tutogef.editpolicy;

import org.eclipse.gef.commands.Command;
import org.eclipse.gef.editpolicies.ConnectionEditPolicy;
import org.eclipse.gef.requests.GroupRequest;

public class AppConnectionDeleteEditPolicy extends ConnectionEditPolicy {

    @Override
    protected Command getDeleteCommand(GroupRequest arg0) {
        ConnectionDeleteCommand command = new ConnectionDeleteCommand();
        command.setLink(getHost().getModel());
        return command;
    }
}





The ConnectionDeleteCommand looks like this:
 

package tutogef.model.command;

import org.eclipse.gef.commands.Command;
import tutogef.model.Connection;

public class ConnectionDeleteCommand extends Command {
   
    private Connection conn;
   
    public void setLink(Object model) {
        this.conn = (Connection)model;
    }
   
    @Override
    public boolean canExecute() {
        if (conn == null)
            return false;
        return true;      
    }
   
    @Override
    public void execute() {
        conn.disconnect();
    }
   
    @Override
    public boolean canUndo() {
        if (conn == null)
            return false;        
        return true;
    }
   
    @Override
    public void undo() {
        conn.connect();
    }
}


In order to enable the ability to connect our Services with arrows we have to add the AppConnectionPolicy in tutogef.part.ServicePart, tell ServicePart to react on fired Events when a Connection was added and two very important methods called getModelSourceConnections and getModelTargetConnections:

    // ...

    @Override
    protected void createEditPolicies() {

    // ...
        installEditPolicy(EditPolicy.GRAPHICAL_NODE_ROLE, new AppConnectionPolicy());

    }
    // ...
    public List getModelSourceConnections() {
        return ((Service)getModel()).getSourceConnectionsArray();
    }
   
    public List getModelTargetConnections() {
        return ((Service)getModel()).getTargetConnectionsArray();
    }

    
    @Override
    public void propertyChange(PropertyChangeEvent evt) {

        // ...
        if (evt.getPropertyName().equals(Node.SOURCE_CONNECTION)) refreshSourceConnections();
        if (evt.getPropertyName().equals(Node.TARGET_CONNECTION)) refreshTargetConnections();

    }
    // ...
 
The AppConnectionPolicy starts the requests to connect source and target. A reconnection is handles separately:


package tutogef.part;

import org.eclipse.gef.EditPart;
import org.eclipse.gef.EditPolicy;
import org.eclipse.gef.Request;
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 tutogef.model.Connection;
import tutogef.model.Node;
import tutogef.model.command.ConnectionCreateCommand;
import tutogef.model.command.ConnectionReconnectCommand;


public class AppConnectionPolicy extends GraphicalNodeEditPolicy {

    @Override
    protected Command getConnectionCompleteCommand(CreateConnectionRequest request) {
        ConnectionCreateCommand cmd = (ConnectionCreateCommand)request.getStartCommand();
        Node targetNode = (Node)getHost().getModel();
        cmd.setTargetNode(targetNode);
        return cmd;
    }

    @Override
    protected Command getConnectionCreateCommand(CreateConnectionRequest request) {
        ConnectionCreateCommand cmd = new ConnectionCreateCommand();
        Node sourceNode = (Node)getHost().getModel();
        cmd.setConnectionType(Integer.parseInt(request.getNewObjectType().toString()));
        cmd.setSourceNode(sourceNode);
        request.setStartCommand(cmd);
        return cmd;
    }

    @Override
    protected Command getReconnectSourceCommand(ReconnectRequest request) {
        Connection conn = (Connection)request.getConnectionEditPart().getModel();
        Node sourceNode = (Node)getHost().getModel();
        ConnectionReconnectCommand cmd = new ConnectionReconnectCommand(conn);
        cmd.setNewSourceNode(sourceNode);       
        return cmd;
    }

    @Override
    protected Command getReconnectTargetCommand(ReconnectRequest request) {
        Connection conn = (Connection)request.getConnectionEditPart().getModel();
        Node targetNode = (Node)getHost().getModel();
        ConnectionReconnectCommand cmd = new ConnectionReconnectCommand(conn);
        cmd.setNewTargetNode(targetNode);       
        return cmd;
    }
}



The Commands for connecting and reconnecting the nodes are very straight forward. We create them in tutogef.model.command starting with the ConnectionCreateCommand:


package tutogef.model.command;

import org.eclipse.gef.commands.Command;

import tutogef.model.Connection;
import tutogef.model.Node;

public class ConnectionCreateCommand extends Command {
   
    private Node sourceNode, targetNode;
    private Connection conn;
    private int connectionType;
   
    public void setSourceNode(Node sourceNode) {
        this.sourceNode = sourceNode;
    }
   
    public void setTargetNode(Node targetNode) {
        this.targetNode = targetNode;
    }
   
    @Override
    public boolean canExecute() {
        if (sourceNode == null || targetNode == null)
            return false;
        else if (sourceNode.equals(targetNode))
            return false;
        return true;
    }
   
    @Override
    public void execute() {
        conn = new Connection(sourceNode, targetNode, connectionType);
        conn.connect();
    }
   
    @Override
    public boolean canUndo() {
        if (sourceNode == null || targetNode == null || conn == null)
            return false;
        return true;         
    }
   
    @Override
    public void undo() {
        conn.disconnect();
    }

    public void setConnectionType(int connectionType) {
        this.connectionType = connectionType;
    }

    public int getConnectionType() {
        return connectionType;
    }
}


The ConnectionReconnectCommand:

package tutogef.model.command;

import org.eclipse.gef.commands.Command;

import tutogef.model.Connection;
import tutogef.model.Node;

public class ConnectionReconnectCommand extends Command {
   
    private Connection conn;
    private Node oldSourceNode;
    private Node oldTargetNode;
    private Node newSourceNode;
    private Node newTargetNode;
   
    public ConnectionReconnectCommand(Connection conn) {
        if (conn == null) {
            throw new IllegalArgumentException();
        }
        this.conn = conn;
        this.oldSourceNode = conn.getSourceNode();
        this.oldTargetNode = conn.getTargetNode();
    }
   
    public boolean canExecute() {
        if (newSourceNode != null) {
            return checkSourceReconnection();
        } else if (newTargetNode != null) {
            return checkTargetReconnection();
        }
        return false;
    }
   
    private boolean checkSourceReconnection() {
        if (newSourceNode == null)
            return false;
        else if (newSourceNode.equals(oldTargetNode))
            return false;
        else if (!newSourceNode.getClass().equals(oldTargetNode.getClass()))
            return false;
        return true;
    }
   
    private boolean checkTargetReconnection() {
        if (newTargetNode == null)
            return false;
        else if (oldSourceNode.equals(newTargetNode))
            return false;
        else if (!oldSourceNode.getClass().equals(newTargetNode.getClass()))
            return false;
        return true;
    }
   
    public void setNewSourceNode(Node sourceNode) {
        if (sourceNode == null) {
            throw new IllegalArgumentException();
        }
        this.newSourceNode = sourceNode;
        this.newTargetNode = null;
    }
   
    public void setNewTargetNode(Node targetNode) {
        if (targetNode == null) {
            throw new IllegalArgumentException();
        }
        this.newSourceNode = null;
        this.newTargetNode = targetNode;
    }
   
    public void execute() {
        if (newSourceNode != null) {
            conn.reconnect(newSourceNode, oldTargetNode);
        } else if (newTargetNode != null) {
            conn.reconnect(oldSourceNode, newTargetNode);
        } else {
            throw new IllegalStateException("Should not happen");
        }
    }
   
    public void undo() {
        conn.reconnect(oldSourceNode,oldTargetNode);
    }
}


There are two things left to do in tutogef.part. First thing is the ServiceParts ability to 'dock' an arrow. For that purpose we implement NodeEditPart to get the so called Anchors:


public class ServicePart extends AppAbstractEditPart implements NodeEditPart{
// ...
   @Override
    public ConnectionAnchor getSourceConnectionAnchor(
            ConnectionEditPart connection) {
       return new ChopboxAnchor(getFigure());
    }

    @Override
    public ConnectionAnchor getSourceConnectionAnchor(Request request) {
        return new ChopboxAnchor(getFigure());
    }

    @Override
    public ConnectionAnchor getTargetConnectionAnchor(
            ConnectionEditPart connection) {
        return new ChopboxAnchor(getFigure());
    }

    @Override
    public ConnectionAnchor getTargetConnectionAnchor(Request request) {
        return new ChopboxAnchor(getFigure());
    }
// ...
}


We are using the easiest method of getting our ServiceParts connected using the ChopboxAnchor. It simply calculates the shortest path from the border to the center of the figure and returns that point. For now we are not considering overlapping anchors.

Also we have to the routine which calculates our connection paths. In EnterprisePart we have to edit createFigure:

public class EnterprisePart extends AppAbstractEditPart {

    @Override
    protected IFigure createFigure() {
    //...
        ConnectionLayer connLayer = (ConnectionLayer)getLayer(LayerConstants.CONNECTION_LAYER);
        connLayer.setAntialias(SWT.ON);
        connLayer.setConnectionRouter(new ShortestPathConnectionRouter(figure));
    //...
    }
}

We are almost done now! The AppEditPartFactory is not aware of the Connection model and ConnectionPart (yet):

public class AppEditPartFactory implements EditPartFactory {

    @Override
    public EditPart createEditPart(EditPart context, Object model) {
    EditPart part;
    // ...
    else if (model instanceof Connection) {
            part = new ConnectionPart();
        }
    // ...
    }
}


Last class we are creating is the ConnectionCreationFactory. Within this Factory we set what kind of connection we want to be created. The rest looks just like the NodeCreationFactory:


package tutogef.model;

import org.eclipse.gef.requests.CreationFactory;

public class ConnectionCreationFactory implements CreationFactory {

    private int connectionType;

    public ConnectionCreationFactory(int connectionType) {
        this.connectionType = connectionType;
    }

    @Override
    public Object getNewObject() {
        return null;
    }

    @Override
    public Object getObjectType() {
        return connectionType;
    }
}



Final step is to add the 3 new tools to our toolbar in tutogef.MyGraphicalEditor. The tool entry uses the just created factory to start the process of connecting two elements with an arrow:


//...
    @Override
    protected PaletteRoot getPaletteRoot() {
        // ...

        PaletteDrawer connectionElements = new PaletteDrawer("Connecting Elements");
        root.add(connectionElements);
       
        connectionElements.add(new ConnectionCreationToolEntry("deliver design","Create Connections",
                new ConnectionCreationFactory(Connection.CONNECTION_DESIGN),
                null,
                null));
       
        connectionElements.add(new ConnectionCreationToolEntry("deliver resources","Create Connections",
                new ConnectionCreationFactory(Connection.CONNECTION_RESOURCES),
                null,
                null));
        connectionElements.add(new ConnectionCreationToolEntry("distribute work packages","Link Layers",
                new ConnectionCreationFactory(Connection.CONNECTION_WORKPACKAGES),
                null,
                null));

        // ...

    }


Now the time has come to try our new feature. Fire up TutoGEF!


Here is the extended TutoGEF project.

28 comments:

R.Amirtharaj said...

Ur tutorials are very nice ...
Plz tell how to save as xml file for this application then reopen again.

nGotme said...

Hey R.Amirtharaj,

thanks - I am giving my best.
For the next few posts I am planning something on .xml saves. Tho I cannot tell yet when that is going to happen.
Check back soon!

Girish Kumar V said...

Hi
The Tutorial is so simple and easy to understand
I've read many tutorials for implementing connections but this tutorial helped me a lot

I'm thinking of implementing the connections on drag and drop

its something like selecting a source node and dropping on the target node then the connection should appear,
I think this can be implemented but need some guidance
can you please help me in achieving this

and i like to have my own connection figure (which should have some other Figure as Child )

NG said...

This project is very useful. I want to somehow extend facility to be able to rename the connection. Trying really hard. Please suggest what steps will be required.

nGotme said...

Thats nice to hear, thanks for the feedback.

First of all you need to give the Connection model a name. (Add String name to the connection model)

There are two ways I see to easily rename the connection now.

1) First way you might want to try is to use the properties of the connection to rename it. (the same way it was done to the Node model)
Create a ConnectionPropertySource in the Image of the already existing NodePropertySource just considering the name property.

2) Look at the RenameAction and edit it so the Connection model/editpart is considered.

Sorry for taking so long to respond. If you run into any more problems let me know.

nGotme said...

@GiRi$H KuM@R

Your request is doable of course but it requires some more work.

The way I understood the source node is not visible yet? In that case the first thing I would think about is a simple LayoutManager. That manager must position the source node after dropping it on the target node in its vicinity.

The target Editpart needs a DropListener which realizes that another editPart or node (model) is being dropped on him.

Within this drop mechanism the layout manager must do its work and the connection can be established by creating a new Request() with a new ConnectionCreateCommand(). (Look at the RenameAction class in TutoGef - there you can find a simple implementation)

The figure of the Connection can be changed. Look at the ConnectionPart in the TutoGEF example. It specifies a PolylineConnection which use the PolylineDecoration() as a template.

The last thing you asked: "which should have some other Figure as Child" I do not understand (yet). Could yoou please elaborate?

And to you too: sorry for taking so long to answer. I am glad to be of further assitance. Also: if you have any news or discovered something worth mentioning please give me some insight. I am always interested.

Anonymous said...

How is about .xml saves...did you finish? Could you post to us?

nGotme said...

yes I have:

http://gefhowto.blogspot.com/2010/12/saving-workbench-on-exit.html

Hope that answers your questions. :)

Anonymous said...

Thank you! I have tried. It ran well..but why is not .xml file..I could not open as .xml file..could you guide me this?

nGotme said...

Well it is not xml because we use the ObjectInput- and -OutputStream.

In order to use xml you need to replace those with you own xml routines. Meaning that you have to save every property of the nodes (position, text, color, children, connections etc...) en capsuled into an xml document.

Maybe in a next post I will get into it. But for now the current way does a good job.

Anonymous said...

Yes,Thanks you!Could you post it soon.

Anonymous said...

hello, How is about save as xml file?
I am looking forward to it.
Plz..guide me..

nGotme said...

Next month I will probably get into the .xml saving part. But no promises. :)

Girish Kumar V said...

Thanks for the reply

As Suggested i'd like to ellobrate on this ...

I've Implemented a TreeStructure Which as Nodes ,The treeFigure has Expand/collapse image corresponding ,onclick of it ,the Nodes get expanded and collopsed correspondingly(This i've achieved )

Nextly i've Two Similar Structures(Just for convinence lets call it as left tree and Right Tree)

I need to establish the connection between node of left Tree's to one of the Right Tree's Node.

This also I some how achieved by overriding getDragTracker of Node's Editpart as below
@Override
public DragTracker getDragTracker(Request request)
{
return new ConnectionDragCreationTool(new ConnectionCreationFactory());

}

Rest of the Things are same as the tutorial says :

Whwnever i drag and drop
1> The connection command is created in the command i 've some checks if the connection is from node to node I'll add a new child to the Center Panel
2> then create a connection between source to this newly added model
3> Then create a connection from that newly added model to target node

Basically i need to have many to many mappings

once this is created i can also select one more left tree node and drop on the center node ,here the connection is simple just adds the entries no intermediate model uis created..

Hope you got ...

Please let me know if you missed anything ....

I thing i like to share is that using drag tracker we can also establish the connection instead of returning the drag tracker you just return ConnectionCreationFactory

But one problem is my other editpolices are not working

I've installed a selectionpolicy and overriden showselection and hideselection these are not working

Then whenever i drag a node towards left tree All the nodes should Expand

So please help in achieving this

Anonymous said...

how is about saving to xml file nGotme?

Vainolo said...
This comment has been removed by the author.
Vainolo said...

Hi. Great tutorial. Thanks for your time, I know how much it takes to make it.
I'm trying to enable direct editing and moving of the labels of the connection but haven't found a way to do this. Do you know how?
thanks

nGotme said...

Hi Vainolo,

I have not implemented direct editing myself yet tho it is on my todo list for quite some time now.

I can only point you to a powerpoint representation I saved the link to in order to get started myself: http://tfs.cs.tu-berlin.de/vila/www_ss08/folien/ppt/7-GEF_Teil3.ppt

On page 14 and following you can find something on the topic. Even though it is in german one might get something out of it.

I hope that helps!

Dzung said...

thanks,it helped me a lot
But I have a question,how to make 2 nodes connect with each other with only ONE connection
This tutorial make 2 node can connect in many connection,I try to write some change in class ConnectionCreateCommand but it doesn't work
Do you have any ideal?
best regard

Dzung said...

problem solved!
:D

nGotme said...

Wonderful, nice to hear that! :) Just for future reference, which class did you alter in order to solve that one connection problem?

Dzung said...

Like I said,I tried to write some change in class ConnectionCreateCommand in public boolean canExecute() and it works
:D
But another question appears,I wonder if nodes want to connect to itself,i try to remove the condition target==source in canExcute() and the connection appear with no target..==!(in the air ^^)
Any ideal for this problem?
best regard!

Dzung said...

Problem solved!
just follow that tutorial:
http://translate.googleusercontent.com/translate_c?hl=vi&rurl=translate.google.com.vn&sl=zh-CN&tl=en&twu=1&u=http://www.cnblogs.com/bjzhanghao/archive/2006/06/22/432553.html&usg=ALkJrhjvbNH5_pgj2m9-IBg5UqBVML0Exw
and fix some code in model class,I'll make the self-connection
Cheer!

nGotme said...

Well done and thanks for the heads up!

LOKESH@NITW said...

THanks for your post.. :D

nGotme said...

I am glad it helps :)

Anonymous said...

I really really thank you for this great tutorial.

vijut said...

hi, the source code download links are not working, can anyone please send me the extended zip file to kalyan.cdev at gmail.com

thanks,
vijay