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
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";private List
// ...
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
return this.sourceConnections;
}
public List
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());
}
}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
return ((Service)getModel()).getSourceConnectionsArray();
}
public List
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;
}
}
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;
}
}
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) {
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());
}
}
@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:
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() {
protected IFigure createFigure() {
//...
ConnectionLayer connLayer = (ConnectionLayer)getLayer(LayerConstants.CONNECTION_LAYER);
connLayer.setAntialias(SWT.ON);
connLayer.setConnectionRouter(new ShortestPathConnectionRouter(figure));
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) {
@Override
public EditPart createEditPart(EditPart context, Object model) {
EditPart part;
// ...
else if (model instanceof Connection) {
part = new ConnectionPart();
}
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!
28 comments:
Ur tutorials are very nice ...
Plz tell how to save as xml file for this application then reopen again.
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!
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 )
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.
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.
@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.
How is about .xml saves...did you finish? Could you post to us?
yes I have:
http://gefhowto.blogspot.com/2010/12/saving-workbench-on-exit.html
Hope that answers your questions. :)
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?
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.
Yes,Thanks you!Could you post it soon.
hello, How is about save as xml file?
I am looking forward to it.
Plz..guide me..
Next month I will probably get into the .xml saving part. But no promises. :)
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
how is about saving to xml file nGotme?
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
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!
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
problem solved!
:D
Wonderful, nice to hear that! :) Just for future reference, which class did you alter in order to solve that one connection problem?
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!
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!
Well done and thanks for the heads up!
THanks for your post.. :D
I am glad it helps :)
I really really thank you for this great tutorial.
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
Post a Comment