Sunday, May 12, 2013

How to avoid overlapping of shapes

While working on the project of creating diagram editor, one of the requirements was to avoid shape overlapping. Graphiti does not have this functionality out of the box, but it offers possibility how you can implement it by yourself. Today will see how to avoid overlapping of shapes (ContainerShapes) in a Diagram.

There are few points where you can forbid overlapping shapes in your diagram. One of the point is the moving feature. In the move feature you can specify when you would like to move the shape, and if you allow to move the shape, to which position you are allowed to move it. In order to achieve this, you need to create your own MoveFeature by extending the DefaultMoveShapeFeature. The two functions that you need to override are canMoveShape, and postMoveShape. In the canMoveShape function, beside constraining the moving based on a business object, we will see how to check if there is already an existing shape on that position. This is done by checking all the relative positions of all children shapes that are of type ContainerShapes (meaning that is shape representing a business object, the most outer shape of some figure). This is done with the following code:


@Override
public boolean canMoveShape(IMoveShapeContext context) {
 boolean canMove = super.canMoveShape(context);

 GraphicsAlgorithm thisGa = context.getShape().getGraphicsAlgorithm();

 EObject targetBo = context.getTargetContainer().getLink().getBusinessObjects().get(0);
 if( targetBo instanceof BusinessObject){
  canMove = true;
 }

 //Check the relative position of all children of the target container
 for(Shape shape : context.getTargetContainer().getChildren()){
  if(shape instanceof ContainerShape && !shape.equals(context.getShape())){ //It is another object
   GraphicsAlgorithm ga = shape.getGraphicsAlgorithm();

   //With only two points we can define the area of the shape
   //e.g:  (x,y)
   //        +---------+
   //       |         |
   //        +---------+
   //              (x1, y1)
   int x  = ga.getX(); //left most x
   int x1 = ga.getX() + ga.getWidth(); //right most x
   int y  = ga.getY(); //left most y
   int y1 = ga.getY() + ga.getHeight(); //right most y

   //If the new position of the moving shape are within one of the children, do not allow moving
   if((context.getX()+thisGa.getWidth()) > x && context.getX() < x1 && (context.getY()+thisGa.getHeight()) > y && context.getY() < y1){
    canMove = false;
   }

  }
 }
 return canMove;
}

Now that we allowed moving of the shape, we an move the shape inside some other shape. Inside its parent the shape can still be half inside the borders, and half outside. But we want to avoid this too. In order to do that we need to override the postMoveShape function. Here we check whether the moved shape is withing the borders of its parent. If it is not we correct it. This is done with the following code:

/**
* Post move correction
*/
@Override
protected void postMoveShape(IMoveShapeContext context) {
 EObject targetBo = context.getTargetContainer().getLink().getBusinessObjects().get(0);
 EObject sourceBo = context.getSourceContainer().getLink().getBusinessObjects().get(0);



 Shape containerShape = context.getShape();
 GraphicsAlgorithm containerGa = containerShape.getGraphicsAlgorithm();

 ContainerShape parentContainer = (ContainerShape) containerShape.eContainer();
 GraphicsAlgorithm parentGa = parentContainer.getGraphicsAlgorithm();


 if(!parentGa.getGraphicsAlgorithmChildren().isEmpty()){
  parentGa = parentGa.getGraphicsAlgorithmChildren().get(0); //If there is a visible rectangle, get the visible then
 }

 //Make sure that the shape is not outside of the the parent visible rectangle
 if(containerGa.getX() <= parentGa.getX()+10){
  containerGa.setX(parentGa.getX()+10);
 }
 if(containerGa.getY() <= parentGa.getY()+30){
  containerGa.setY(parentGa.getY()+30);
 }

 if((containerGa.getX()+containerGa.getWidth()) > (parentGa.getX()+parentGa.getWidth())){
  int x = (parentGa.getX()+parentGa.getWidth())-containerGa.getWidth()-10;
  containerGa.setX(x);
 }

 if((containerGa.getY()+containerGa.getHeight()) > (parentGa.getY()+parentGa.getHeight())){
  int y = (parentGa.getY()+parentGa.getHeight()) - containerGa.getHeight() - 10;
  containerGa.setY(y);
 }
}

You can also restrict negative coordinates of a shape by overriding avoidNegativeCoordinates from the DeafaultMoveShapeFeature.
The final MoveFeature looks like this:

import org.eclipse.emf.ecore.EObject;
import org.eclipse.graphiti.features.IFeatureProvider;
import org.eclipse.graphiti.features.context.IMoveShapeContext;
import org.eclipse.graphiti.features.impl.DefaultMoveShapeFeature;
import org.eclipse.graphiti.mm.algorithms.GraphicsAlgorithm;
import org.eclipse.graphiti.mm.pictograms.ContainerShape;
import org.eclipse.graphiti.mm.pictograms.Shape;


public class MoveFeature extends DefaultMoveShapeFeature{

 public MoveFeature(IFeatureProvider fp) {
  super(fp);
 }

 protected boolean avoidNegativeCoordinates() {
  return true;
 }

 @Override
 public boolean canMoveShape(IMoveShapeContext context) {
  boolean canMove = super.canMoveShape(context);

  GraphicsAlgorithm thisGa = context.getShape().getGraphicsAlgorithm();

  EObject targetBo = context.getTargetContainer().getLink().getBusinessObjects().get(0);
  if( targetBo instanceof BusinessObject){
   canMove = true;
  }

  //Check the relative position of all children of the target container
  for(Shape shape : context.getTargetContainer().getChildren()){
   if(shape instanceof ContainerShape && !shape.equals(context.getShape())){ //It is another object
    GraphicsAlgorithm ga = shape.getGraphicsAlgorithm();

    //With only two points we can define the area of the shape
    //e.g:  (x,y)
    //        +---------+
    //        |         |
    //        +---------+
    //              (x1, y1)
    int x  = ga.getX(); //left most x
    int x1 = ga.getX() + ga.getWidth(); //right most x
    int y  = ga.getY(); //left most y
    int y1 = ga.getY() + ga.getHeight(); //right most y

    //If the new position of the moving shape are within one of the children, do not allow moving
    if((context.getX()+thisGa.getWidth()) > x && context.getX() < x1 && (context.getY()+thisGa.getHeight()) > y && context.getY() < y1){
     canMove = false;
    }

   }
  }
  return canMove;
 }


 /**
  * Post move correction
  */
 @Override
 protected void postMoveShape(IMoveShapeContext context) {
  EObject targetBo = context.getTargetContainer().getLink().getBusinessObjects().get(0);
  EObject sourceBo = context.getSourceContainer().getLink().getBusinessObjects().get(0);
  
  
  Shape containerShape = context.getShape();
  GraphicsAlgorithm containerGa = containerShape.getGraphicsAlgorithm();

  ContainerShape parentContainer = (ContainerShape) containerShape.eContainer();
  GraphicsAlgorithm parentGa = parentContainer.getGraphicsAlgorithm();

  
  if(!parentGa.getGraphicsAlgorithmChildren().isEmpty()){
   parentGa = parentGa.getGraphicsAlgorithmChildren().get(0); //If there is a visible rectangle, get the visible then
  }

  //Make sure that the shape is not outside of the the parent visible rectangle
  if(containerGa.getX() <= parentGa.getX()+10){
   containerGa.setX(parentGa.getX()+10);
  }
  if(containerGa.getY() <= parentGa.getY()+30){
   containerGa.setY(parentGa.getY()+30);
  }

  if((containerGa.getX()+containerGa.getWidth()) > (parentGa.getX()+parentGa.getWidth())){
   int x = (parentGa.getX()+parentGa.getWidth())-containerGa.getWidth()-10;
   containerGa.setX(x);
  }

  if((containerGa.getY()+containerGa.getHeight()) > (parentGa.getY()+parentGa.getHeight())){
   int y = (parentGa.getY()+parentGa.getHeight()) - containerGa.getHeight() - 10;
   containerGa.setY(y);
  }
 }
}

Other point where you can avoid overlapping of shapes is when you add new feature to your Diagram. In the CreateFeature, canCreate function, you can apply the same code from your canMoveShape function and this way you can forbid creating shape if there is not enough space on your diagram on the place you want to add your feature.
Here you should know, that this will prevent you from creating this feature at all, if there is not enough space on your Diagram, so be careful with this restriction here.

Finally lets see how this looks like in action. From the picture you can't see the forbidden icon, but your diagram won't allow you to do this:
Figure 1: Overlapping of shapes
Also with this code you won't face this situation where half of your shape is inside, and half is outside your parent shape:
Figure 2: Overlapping of shapes 2


If you have further questions I will be happy to help you.

Saturday, May 11, 2013

First post: Collapse features

Hello everybody! I recently started working on a project that involves creating a visual diagram editor in Eclipse Graphiti. As a thank-you to the open source community I created this blog with a hope that I will help  others, and save novice and advance users a lot of hours searching solutions on internet.

Before I start with my first post, I should mention that most of the help you can find it on http://wiki.eclipse.org/GMP/Graphiti wiki page, and on http://www.eclipse.org/forums/index.php/f/187/ forum.

I assume that the you already know something about Eclipse Graphiti, if not please read the Graphiti developer guide from here.

Let me start with my first tutorial, about how to create a collapsible feature, and how to use it in your diagram. I won't provide the model classes, but I will use the generic name BusinessObject which will indicate your model class.

First you need to create a custom feature that will do the collapsing and extending of your shapes. Lets call this feature CollapseFeature and this will override the AbstractCustomFeature. The two most important functions that need to be overridden are canExecute, and execute. In the first one you tell whether the target shape can be collapsed or not. Here is the complete java code of the CollapseFeature class.

import java.util.Iterator;

import org.eclipse.graphiti.features.IFeatureProvider;
import org.eclipse.graphiti.features.IResizeShapeFeature;
import org.eclipse.graphiti.features.context.IContext;
import org.eclipse.graphiti.features.context.ICustomContext;
import org.eclipse.graphiti.features.context.impl.ResizeShapeContext;
import org.eclipse.graphiti.features.custom.AbstractCustomFeature;
import org.eclipse.graphiti.mm.algorithms.Ellipse;
import org.eclipse.graphiti.mm.pictograms.Anchor;
import org.eclipse.graphiti.mm.pictograms.Connection;
import org.eclipse.graphiti.mm.pictograms.ContainerShape;
import org.eclipse.graphiti.mm.pictograms.PictogramElement;
import org.eclipse.graphiti.mm.pictograms.Shape;
import org.eclipse.graphiti.services.Graphiti;


public class CollapseFeature extends AbstractCustomFeature{

 
 public CollapseFeature(IFeatureProvider fp) {
  super(fp);
 }

 @Override
 public boolean canExecute(ICustomContext context) {
  boolean ret = false;
  PictogramElement[] pes = context.getPictogramElements();
  if (pes != null && pes.length == 1) {
   Object bo = getBusinessObjectForPictogramElement(pes[0]);
   //Add more of the objects that collapse here
   if (bo instanceof BusinessObject) {
    ret = true;
   }
  }
  return ret;
 }


 @Override
 public boolean isAvailable(IContext context) {

  return true;
 }

 public void execute(ICustomContext context) {
  PictogramElement[] pes = context.getPictogramElements();
  if (pes != null && pes.length == 1) {
   Object bo = getBusinessObjectForPictogramElement(pes[0]);

   if(bo instanceof BusinessObject){
    collapseShape(pes[0]);
   }
  }
   
 }

 /**
  * Collapse the shape for the BusinessObject
  * 
  * @param pe PictogamElement for the shape of the object
  */
 public void collapseShape(PictogramElement pe){
  ContainerShape cs = (ContainerShape) pe;
  int width = pe.getGraphicsAlgorithm().getWidth();
  int height = pe.getGraphicsAlgorithm().getHeight();  


  int changeWidth = 0;
  int changeHeight = 0;


  boolean visible = false;
  if(Graphiti.getPeService().getPropertyValue(pe, "isCollapsed") == null 
    || Graphiti.getPeService().getPropertyValue(pe, "isCollapsed").equals("false")){

   Graphiti.getPeService().setPropertyValue(pe, "initial_width", String.valueOf(width));
   Graphiti.getPeService().setPropertyValue(pe, "initial_height", String.valueOf(height));
   visible = false;
  }else if(Graphiti.getPeService().getPropertyValue(pe, "isCollapsed") != null 
    && Graphiti.getPeService().getPropertyValue(pe, "isCollapsed").equals("true")){
   changeWidth = Integer.parseInt(Graphiti.getPeService().getPropertyValue(pe, "initial_width"));
   changeHeight = Integer.parseInt(Graphiti.getPeService().getPropertyValue(pe, "initial_height"));
   Graphiti.getPeService().setPropertyValue(pe, "isCollapsed", "false");
   visible = true;
  }

  ResizeShapeContext context1 = new ResizeShapeContext(cs);
  context1.setSize(changeWidth, changeHeight);
  context1.setLocation(cs.getGraphicsAlgorithm().getX(), cs.getGraphicsAlgorithm().getY());
  IResizeShapeFeature rsf = getFeatureProvider().getResizeShapeFeature(context1);
  if (rsf.canExecute(context1)) {
   rsf.execute(context1);
  }

  if(!visible){
   Graphiti.getPeService().setPropertyValue(pe, "isCollapsed", "true");
  }
  //visible/invisible all the children
  makeChildrenInvisible(cs, visible);
 }

       /**
  * Recursive function that makes all the children inside a shape visible/invisible
  * 
  * @param cs ContainerShape
  * @param visible true/false
  */
 public void makeChildrenInvisible(ContainerShape cs, boolean visible){
  if(cs.getChildren().isEmpty()){
   return;
  }else{
   Iterator iter = cs.getChildren().iterator();
   while (iter.hasNext()) {
    Shape shape = iter.next();

    if(shape instanceof ContainerShape){ //It is another shape
     makeChildrenInvisible((ContainerShape) shape, visible);


     shape.setVisible(visible);
     Anchor anchr = shape.getAnchors().get(0);
     boolean initVisible = false;

     //Check whether the initial shape is visible or not
     for(Shape shape1 : ((ContainerShape) shape).getChildren()){
      if(shape1.getGraphicsAlgorithm() instanceof Ellipse){
       initVisible = shape1.isVisible();
      }
     }

     for(int i=0; i < anchr.getIncomingConnections().size(); i++){
      Connection conn = anchr.getIncomingConnections().get(i);
      if(initVisible){ //Change visibility only to visible connections
       conn.setVisible(visible);
       for(int j=0; j< conn.getConnectionDecorators().size(); j++){
        conn.getConnectionDecorators().get(j).setVisible(visible);
       }
      }
     }
     for(int i=0; i < anchr.getOutgoingConnections().size(); i++){

      Connection conn = anchr.getOutgoingConnections().get(i);
      conn.setVisible(visible);
      for(int j=0; j< conn.getConnectionDecorators().size(); j++){
       conn.getConnectionDecorators().get(j).setVisible(visible);
      }
     }
    }
   }
  }
 }
}

Most of the job is done in the private function collapseShape. This function saves the initial width and initial height of the shape, and also saves a state if this shape is collapsed already or not. After that it creates a ResizeShapeContext and calls it for this shapes. You should know each feature can have its own ResizeShapeFeature defined in the DialogueFeatureProvider. The code from line 88-95 does just that. It gets the ResizeShapeFeature if it is defined for this context (this specific feature), if not it uses the default one, and it executes the resizing. The line 96-98 is very important to be after the resizing, and I will tell you later why.

The trickiest function here is makeChildrenInvisible which recursively goes for each children of the shape and sets its visibility accordingly. It also sets the visibility of the incoming and outgoing connections. In the incoming connections I firs check if they are visible. This is because I have a artificial connection from a shape within the same container shape. You can ignore this part, and the part that checks whether this connection is visible or not. But maybe this will give some inspirations for your code :) (Line 125-130).

Next part, is to define a context button in the button pad based on this custom feature. This is done in your DialogueToolBehaviourProvider class in the function getContextButtonPad, with the following code:
 @Override
 public IContextButtonPadData getContextButtonPad(IPictogramElementContext context) {

  IContextButtonPadData data = super.getContextButtonPad(context);
  PictogramElement pe = context.getPictogramElement();

  EObject bo = (EObject) getFeatureProvider().getBusinessObjectForPictogramElement(pe);

  // 1. set the generic context buttons
  // note, that we do not add 'remove' (just as an example)
  setGenericContextButtons(data, pe, CONTEXT_BUTTON_DELETE | CONTEXT_BUTTON_UPDATE );

  // 2. set the collapse button 
  CustomContext cc = new CustomContext(new PictogramElement[] { pe });
  ICustomFeature[] cf = getFeatureProvider().getCustomFeatures(cc);
  for (int i = 0; i < cf.length; i++) {
   ICustomFeature iCustomFeature = cf[i];
   if (iCustomFeature instanceof CollapseFeature && iCustomFeature.canExecute(cc)) {
    
    String image = IPlatformImageConstants.IMG_EDIT_COLLAPSE;
    String collapseExpand = "Collapse";
    if(Boolean.parseBoolean(Graphiti.getPeService().getPropertyValue(pe, "isCollapsed"))){
     image = IPlatformImageConstants.IMG_EDIT_EXPAND;
     collapseExpand = "Expand";
    }
    String name = "";
    
    if(bo instanceof BusinessObject){
     BusinessObject bo2= (BusinessObject)bo;
     
     if(bo2!=null && bo2.getName()!=null){
      name = bo2.getName();
     }
    }
    
    IContextButtonEntry collapseButton  = new ContextButtonEntry(iCustomFeature, cc); 
    collapseButton.setDescription(collapseExpand+" "+name);
    collapseButton.setText(collapseExpand);
    collapseButton.setIconId(image);
      
    data.setCollapseContextButton(collapseButton);
    break;
   }
  }
  
  return data;
 }

The thing I mention earlier, why you should have the property isCollapsed set to true is to be able to show the collapse and expand images. I had problems if they are not in that order.

This is how it looks finally on a running diagram:
Figure 1: Not collapsed shape
Figure 2: Collapsed shape

Maybe the above code is not generic enough and it is more suitable for my needs, but I am sure you will be able to adjust it to your needs. If not, don't hesitate to drop me a question.


That is all for today. I am very happy to read your comments and answer your questions if you have any.

Till the next time.