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.

No comments:

Post a Comment