Viewpoint - Provide custom arrange-all

Contents

1   Goals

One of the main issues of visual modelers is the global arrangement of all elements. GMF provides a default implementation of an "Arrange All" algorithm to solve this issue but it happens that this default behavior is not adapted for some diagrams. Imagine a diagram representing the hierarchy between a program classes, the "natural" representation a human thinks about is that of a tree. When you add associations on such diagrams, GMF won't keep the tree representation of the diagram. Viewpoint provides an API to arrange diagrams according to custom business rules.

2   The arrange all concept as implemented by GMF

GMF provides basic API and implementations to ease the customization of the "Arrange All" mechanism. It also implements a complete algorithm that is based on the graphical elements.

The top level type of this this API is ILayoutNodeProvider

./images/arrange-all/arrange-all-tlinterface.jpg

As you can see, this interface is designed for genericity. Fortunately, GMF provides abstract classes that handle the crappy work while letting you define the business rules :

./images/arrange-all/arrange-all-ls.jpg

This can be done by implementing three operations :

The provides operation is meant to return true if the class can arrange the diagram for the specified operation.

The layoutEditParts operations will return the commands that will actually be in charge of arranging the diagrams' edit parts. The first one takes the main container that is to be arranged while the latter accepts a list of edit parts to arrange.

3   The LayoutProviders as provided by Viewpoint

The class AbstractLayoutEditPartProvider is the simplest one. Viewpoint provides utility classes to ease the implementation of "Arrange All" algorithms :

./images/arrange-all/arrange-all-air.jpg

Here is a list of all provided layout providers and a basic description of each :

4   The ViewOrderings

4.1   The API

./images/arrange-all/viewordering.jpg

All LayoutProviders provided by Viewpoint can be used with a ViewOrdering which has the responsibility of actually ordering the views. Here is the ViewOrdering API :

The aim of all these classes is to order the GMF views and to provide this result to an AbstractAirLayoutProvider.

4.2   Sample

Here is a sample ordering all views to get a tree from a list of UML2 packages :

/**
 * Orders packages.
 *
 * @author ymortier
 */
public class PackageTreeOrdering extends SemanticTreeOrdering {

    /**
     * {@inheritDoc}
     *
     * @see fr.obeo.dsl.viewpoint.diagram.graphical.edit.layout.api.SemanticTreeOrdering#getSemanticChildren(org.eclipse.emf.ecore.EObject,
     *      java.util.List)
     */
    public List getSemanticChildren(EObject semanticParent, List candidates) {
        List result = Collections.EMPTY_LIST;
        if (semanticParent instanceof Package) {
            result = new LinkedList<Package>(((Package) semanticParent).getNestedPackages());
            result.retainAll(candidates);
        }
        return result;
    }

    /**
     * {@inheritDoc}
     *
     * @see fr.obeo.dsl.viewpoint.diagram.graphical.edit.layout.api.SemanticTreeOrdering#getSemanticRoots(java.util.List)
     */
    public List getSemanticRoots(List objects) {
        List<Package> roots = new LinkedList<Package>();
        for (Object object : objects) {
            EObject semantic = (EObject) object;
            if (semantic instanceof Package) {
                Package package_ = (Package) semantic;
                if (package_.eContainer() == null || !objects.contains(package_.eContainer())) {
                    roots.add(package_);
                }
            }
        }
        return roots;
    }

}

This tells us how to order the packages. We must notify Viewpoint that this ViewOrdering has to be used for the Package Hierarchy diagram.

4.3   The ViewOrderingProvider

The providers API :

Here is the implementation of a ViewOrderingProvider for an UML2 modeler :

/**
 * The view ordering provider for UML2 modeler.
 *
 * @author ymortier
 */
public class Uml2ViewOrderingProvider implements ViewOrderingProvider {

    /** The classifier ordering. */
    private ClassifierTreeOrdering classifierOrdering = new ClassifierTreeOrdering();

    /**
     * Default constructor.
     */
    public Uml2ViewOrderingProvider() {
        classifierOrdering.setUserAwareCapable(true);
    }

    /**
     * @see fr.obeo.dsl.viewpoint.diagram.graphical.edit.layout.api.ViewOrderingProvider#getViewOrdering(viewpoint.description.ViewPointElementMapping)
     */
    public ViewOrdering getViewOrdering(DiagramElementMapping mapping) {
        return (ViewOrdering) getMappingToViewOrdering().get(getMappingName(mapping));
    }

    /**
     * @see fr.obeo.dsl.viewpoint.diagram.graphical.edit.layout.api.ViewOrderingProvider#provides(viewpoint.description.ViewPointElementMapping)
     */
    public boolean provides(DiagramElementMapping mapping) {
        return getMappingToViewOrdering().containsKey(getMappingName(mapping));
    }

    private String getMappingName(DiagramElementMapping mapping) {
        String result = null;
        if (mapping instanceof NodeMapping) {
            result = ((NodeMapping) mapping).getName();
        } else if (mapping instanceof EdgeMapping) {
            result = ((EdgeMapping) mapping).getName();
        } else if (mapping instanceof ContainerMapping) {
            result = ((ContainerMapping) mapping).getName();
        }
        return result;
    }

    /**
     * Returns a map : MappingName(String) -> ViewOrdering.
     *
     * @return a map : MappingName(String) -> ViewOrdering.
     */
    private Map getMappingToViewOrdering() {
        Map result = new HashMap();
        result.put(Uml2Constants.LIFELINE_MAPPING_NAME, new LifelineOrdering());
        result.put(Uml2Constants.MESSAGE_MAPPING_NAME, new MessageOrdering());
        result.put(Uml2Constants.CLASS_MAPPING_NAME, this.classifierOrdering);
        result.put(Uml2Constants.INTERFACE_MAPPING_NAME, this.classifierOrdering);
        return result;
    }

}

Once this has been implemented, we need to add the extension to the plugin.xml :

./images/arrange-all/arrange-all-provider-extension.png
<extension
      point="fr.obeo.dsl.viewpoint.diagram.viewOrderingProvider">
   <viewOrderingProvider
         providerClass="fr.obeo.sample.advance.uml2.specific.provider.Uml2ViewOrderingProvider">
   </viewOrderingProvider>
</extension>

And that concludes the addition of a view ordering provider. Simple, wasn't it?

5   How to map a Layout Provider with a Diagram

Now, we know how to both order and arrange views. We still need to let Viewpoint know that we want to use a custom Layout Provider for a specific Diagram.

./images/arrange-all/airlayoutprovider.jpg

Here is a provider designed for an UML2 modeler :

/**
 * @author ymortier
 *
 */
public class Uml2AirLayoutProvider implements AirLayoutProvider {

    /** The GMF layout provider. */
    private CompoundLayoutProvider layoutProvider;

    /** The class diagram layout provider. */
    private AirGridLayoutProvider classDiagramLayoutProvider;

    /** The package hierarchy layout provider. */
    private AirGridLayoutProvider packageHierarchyLayoutProvider;

    /**
     * @see fr.obeo.dsl.viewpoint.transversal.edit.layout.api.AirLayoutProvider#getLayoutNodeProvider(org.eclipse.gmf.runtime.diagram.ui.editparts.IGraphicalEditPart)
     */
    public AbstractLayoutEditPartProvider getLayoutNodeProvider(IGraphicalEditPart container) {
        if (isSequenceDiagram(container)) {
            if (this.layoutProvider == null) {
                this.layoutProvider = new CompoundLayoutProvider();
                AirLineLayoutProvider airLineLayoutProvider = new AirLineLayoutProvider();
                airLineLayoutProvider.getPadding().right = 80;
                airLineLayoutProvider.setHorizontal(true);
                this.layoutProvider.addProvider(airLineLayoutProvider);
                AirInlineEdgeLayoutProvider airInlineEdgeLayoutProvider = new AirInlineEdgeLayoutProvider();
                airInlineEdgeLayoutProvider.setSide(PositionConstants.EAST_WEST);
                airInlineEdgeLayoutProvider.setStart(PositionConstants.RIGHT);
                airInlineEdgeLayoutProvider.setAlignment(PositionConstants.VERTICAL);
                airInlineEdgeLayoutProvider.setChangeNodeHeight(true);
                airInlineEdgeLayoutProvider.setChangeNodeWidth(true);
                airInlineEdgeLayoutProvider.getPaddings().top = 50;
                this.layoutProvider.addProvider(airInlineEdgeLayoutProvider);
            }
            return this.layoutProvider;
        } else if (isClassDiagram(container)) {
            return this.getClassDiagramLayoutProvider();
        } else if (isPackageHierarchyDiagram(container)) {
            return this.getPackageHierarchyLayoutProvider();
        }
        return null;
    }

    /**
     * @see fr.obeo.dsl.viewpoint.transversal.edit.layout.api.AirLayoutProvider#provides(org.eclipse.gmf.runtime.diagram.ui.editparts.IGraphicalEditPart)
     */
    public boolean provides(IGraphicalEditPart container) {
        return (isClassDiagram(container) && isAbleToLayoutClassDiagram(container)) || isSequenceDiagram(container) || isPackageHierarchyDiagram(container);
    }

    private boolean isSequenceDiagram(IGraphicalEditPart container) {
        if (container instanceof AbstractDDiagramEditPart) {
            AbstractDDiagramEditPart editPart = (AbstractDDiagramEditPart) container;
            if (editPart.resolveSemanticElement() instanceof ViewPoint) {
                ViewPoint viewPoint = (ViewPoint) editPart.resolveSemanticElement();
                if (viewPoint.getDescription() != null) {
                    DiagramDescription viewPointDescription = viewPoint.getDescription();
                    return Uml2Constants.SEQUENCE_DIAGRAM_DESCRIPTION_NAME.equals(viewPointDescription.getName());
                }
            }
        }
        return false;
    }

    private boolean isClassDiagram(IGraphicalEditPart container) {
        if (container instanceof AbstractDDiagramEditPart) {
            AbstractDDiagramEditPart editPart = (AbstractDDiagramEditPart) container;
            if (editPart.resolveSemanticElement() instanceof ViewPoint) {
                ViewPoint viewPoint = (ViewPoint) editPart.resolveSemanticElement();
                if (viewPoint.getDescription() != null) {
                    DiagramDescription viewPointDescription = viewPoint.getDescription();
                    return Uml2Constants.CLASS_DIAGRAM_DESCRIPTION_NAME.equals(viewPointDescription.getName());
                }
            }
        }
        return false;
    }

    private boolean isPackageHierarchyDiagram(IGraphicalEditPart container) {
        if (container instanceof AbstractDDiagramEditPart) {
            AbstractDDiagramEditPart editPart = (AbstractDDiagramEditPart) container;
            if (editPart.resolveSemanticElement() instanceof ViewPoint) {
                ViewPoint viewPoint = (ViewPoint) editPart.resolveSemanticElement();
                if (viewPoint.getDescription() != null) {
                    DiagramDescription viewPointDescription = viewPoint.getDescription();
                    return Uml2Constants.PACKAGE_HIERARCHY_DIAGRAM_DESCRIPTION_NAME.equals(viewPointDescription.getName());
                }
            }
        }
        return false;
    }

    private boolean isAbleToLayoutClassDiagram(IGraphicalEditPart container) {
        if (container instanceof DiagramEditPart) {
            int nbNoInherance = 0;
            int nbInheritance = 0;
            Iterator iterConnections = ((DiagramEditPart) container).getConnections().iterator();
            while (iterConnections.hasNext()) {
                ConnectionEditPart connectionEditPart = (ConnectionEditPart) iterConnections.next();
                EObject semantic = connectionEditPart.resolveSemanticElement();
                if (semantic != null && semantic instanceof DecorateSemanticElement) {
                    EObject realSemantic = ((DecorateSemanticElement) semantic).getTarget();
                    if (realSemantic instanceof Generalization) {
                        nbInheritance++;
                    } else {
                        nbNoInherance++;
                    }
                } else {
                    nbNoInherance++;
                }
            }
            return nbInheritance > nbNoInherance;
        }
        return false;
    }

    /**
     * Returns the class diagram layout provider.
     *
     * @return the class diagram layout provider.
     */
    public AirGridLayoutProvider getClassDiagramLayoutProvider() {
        if (classDiagramLayoutProvider == null) {
            classDiagramLayoutProvider = new AirGridLayoutProvider();
            classDiagramLayoutProvider.setColumnSizeMode(AirGridLayoutProvider.DIMENSION_BY_LINE_OR_COLUMN);
            classDiagramLayoutProvider.setLineSizeMode(AirGridLayoutProvider.DIMENSION_BY_LINE_OR_COLUMN);
            classDiagramLayoutProvider.getPadding().top = 20;
            classDiagramLayoutProvider.getPadding().bottom = 20;
            classDiagramLayoutProvider.getPadding().left = 20;
            classDiagramLayoutProvider.getPadding().right = 20;
        }
        return classDiagramLayoutProvider;
    }

    /**
     * @return the packageHierarchyLayoutProvider
     */
    public AirGridLayoutProvider getPackageHierarchyLayoutProvider() {
        if (packageHierarchyLayoutProvider == null) {
            packageHierarchyLayoutProvider = new AirGridLayoutProvider();
            packageHierarchyLayoutProvider.setColumnSizeMode(AirGridLayoutProvider.DIMENSION_BY_LINE_OR_COLUMN);
            packageHierarchyLayoutProvider.setLineSizeMode(AirGridLayoutProvider.DIMENSION_BY_LINE_OR_COLUMN);
            packageHierarchyLayoutProvider.getPadding().top = 20;
            packageHierarchyLayoutProvider.getPadding().bottom = 20;
            packageHierarchyLayoutProvider.getPadding().left = 30;
            packageHierarchyLayoutProvider.getPadding().right = 30;
        }
        return packageHierarchyLayoutProvider;
    }

    public boolean isDiagramLayoutProvider() {
        return false;
    }

}

Finally, we add the extension in the plug-in file :

./images/arrange-all/arrange-all-layoutprovider-extension.png
<extension
      point="fr.obeo.dsl.common.ui.airLayoutProvider">
   <airLayoutProvider
         priority="high"
         providerClass="fr.obeo.sample.advance.uml2.specific.provider.Uml2AirLayoutProvider">
   </airLayoutProvider>
</extension>

6   Enable the bordered nodes layout on a custom LayoutProvider

The bordered nodes layout arranges all the bordered nodes which are connected to one edge. This arrange reduces the path of the edge between each extremity.

Currently, the layout of the bordered nodes is activated on the default Layout Provider (AirCompositeDownTopProvider, AirCompositeLeftRightProvider and OrderedTreeLayoutProvider. To enable the bordered nodes layout on a new LayoutProvider, in the method getLayoutNodeProvider(IGraphicalEditPart), you must create a new BorderItemAwareLayoutProvider with your own LayoutProvider in parameter. This new BorderItemAwareLayoutProvider must be return instead of your own.

Example of the OrderedTreeLayoutProvider

public AbstractLayoutEditPartProvider getLayoutNodeProvider(final IGraphicalEditPart container) {
    if (isDDiagramWithConfiguredOrderedTreeLayout(container)) {
        //Get the expected layout provider
        AbstractAirLayoutProvider layoutNodeProvider = getAirGridLayoutProvider();
        //Create new BorderItemAwareLayoutProvider
        layoutNodeProvider = BorderItemAwareLayoutProviderHelper.createBorderItemAwareLayoutProvider(layoutNodeProvider);
        //Return the BorderItemAwareLayoutProvider which wrap the previous one
        return layoutNodeProvider;
    } else {
        return null;
    }
}