Sirius – Provide custom style

Goals

Sirius provides tools to design and display diagrams. These diagrams are essentially composed of figures of distinct styles
and shapes and, even though the tooling initially allows for the usage of a wide array of styles, many diagrams will still require
customized styles to conform to the company’s theme.

Sirius offers three distinct ways of customizing its graphical styles:

  1. the StyleConfiguration extension point.
  2. the CustomStyle in addition to the EditPartProvider GMF extension point.
  3. the GMF-provided extension points.

StyleConfiguration

StyleConfiguration is a mechanism that enables the customization of a specific style. It aims at providing support for different
ways of displaying labels, anchors, ...

The StyleConfiguration interface

This interface defines a set of methods that need be implemented in order for the syle to be customizable:

adaptNodeLabel

This method can be used to alter the size and location of a given label according to a given node.

getBorderItemLocatorProvider

This method returns the instance currently providing border item locators for the node. A border item is a node located on the border of another node.

getNameBorderItemLocator

This method returns the item locator for the label of the node. This method is meaningless if the position of the label isn’t BORDER.

adaptViewNodeSizeWithLabel

This method returns the minimum width a node needs to be correctly displayed with its label.

getAnchorProvider

This method returns the instance providing connections anchors.

getLabelIcon

This method returns the icon that will displayed for this element’s representations.

fitToText

This method returns the optimal dimension of the node.

Providing custom StyleConfiguration

The StyleConfiguration mechanism is only available for ViewNode at the moment. Default configurations are initially installed on nodes.

SimpleStyleConfiguration

This configuration is the simplest implementation.

SquareStyleConfiguration

This is the minimal configuration for nodes using the BundledImage style with a square shape.

SimpleSquareStyleConfiguration

This is the minimal configuration for nodes with a Square style.

Providers of StyleConfiguration

Installing your own configurations can be done through the org.eclipse.sirius.diagram.styleConfigurationProvider extension
point. This extension point requires only one attribute: the qualified name of the provider class.

<extension point="org.eclipse.sirius.diagram.styleConfigurationProvider">
   <styleConfigurationProvider providerClass="com.example.diagseq.style.DiagSeqStyleConfigurationProvider" />
</extension>

Here is a sample of the extension tab of the PDE editor when filling in this extension point:

This example records the DiagSeqStyleConfigurationProvider as a configuration provider. This class must implement the
IStyleConfigrationProvider interface.

The latter interface defines two methods:

boolean provides(DiagramElementMapping mapping, Style style)

This method returns true if the provider provides a configuration for the given mapping and style.

StyleConfiguration createStyleConfiguration(DiagramElementMapping mapping, Style style)

This method creates a StyleConfiguration for the specified mapping and style.

Here is an example of a provider:

public class DiagSeqStyleConfigurationProvider implements IStyleConfigurationProvider {
    public StyleConfiguration createStyleConfiguration(DiagramElementMapping mapping, Style style) {
        if (mapping instanceof NodeMapping) {
            NodeMapping nodeMapping = (NodeMapping) mapping;
            if (nodeMapping.getName() != null && nodeMapping.getName().equals(DiagSeqConstants.INSTANCE_ROLE_MAPPING_NAME)) {
                return new InstanceRoleStyleConfiguration();
            }
            if (nodeMapping.getName() != null && nodeMapping.getName().equals(DiagSeqConstants.EXECUTION_MAPPING_NAME)) {
                return new ExecutionSpecificationStyleConfiguration();
            }
        }
        return null;
    }

    public boolean provides(DiagramElementMapping mapping, Style style) {
        return mapping instanceof NodeMapping && ((NodeMapping) mapping).getName() != null
                && (((NodeMapping) mapping).getName().equals(DiagSeqConstants.INSTANCE_ROLE_MAPPING_NAME)
                	|| DiagSeqConstants.EXECUTION_MAPPING_NAME.equals(((NodeMapping) mapping).getName()));
    }
}


Example of a StyleConfiguration implementation

public class SimpleStyleConfiguration implements StyleConfiguration {
    public void adaptNodeLabel(DNode node, WrapLabel nodeLabel) {
        if (nodeLabel.getParent() != null) {
            Rectangle constraint = nodeLabel.getParent().getBounds().getCopy();

            Insets borderDimension = this.getBorderDimension(node);
            constraint.height -= (borderDimension.top + borderDimension.bottom);
            constraint.width -= (borderDimension.left + borderDimension.right);
            constraint.x += borderDimension.left;
            constraint.y += borderDimension.top;

            nodeLabel.setBounds(constraint);
            nodeLabel.getParent().setConstraint(nodeLabel, constraint);
        }
    }

    public int adaptViewNodeSizeWithLabel(DNode viewNode, WrapLabel nodeLabel, int nodeWidth) {
        if (viewNode.getResizeKind() != ResizeKind.NONE_LITERAL) {

        }
        return nodeWidth;
    }

    public AnchorProvider getAnchorProvider() {
        return null;
    }

    public BorderItemLocatorProvider getBorderItemLocatorProvider() {
        return DefaultBorderItemLocatorProvider.getInstance();
    }

    public IBorderItemLocator getNameBorderItemLocator(DNode node, IFigure mainFigure) {
        BorderItemLocator locator = new AirBorderItemLocator(mainFigure, PositionConstants.NSEW);
        locator.setBorderItemOffset(new Dimension(1, 1));
        return locator;
    }

    public Image getLabelIcon(DDiagramElement vpElement) {
        EObject target = vpElement;
        if (vpElement instanceof DSemanticDecorator) {
            target = ((DSemanticDecorator) vpElement).getTarget();
        }
        if (isShowIcon(vpElement)) {
            if (target != null) {
                IItemLabelProvider labelProvider = (IItemLabelProvider) SiriusDiagramEditorPlugin.getInstance()
                		.getItemProvidersAdapterFactory().adapt(target, IItemLabelProvider.class);
                if (labelProvider != null) {
                    ImageDescriptor descriptor = ExtendedImageRegistry.getInstance().getImageDescriptor(labelProvider.getImage(target));
                    if (descriptor == null) {
                        descriptor = ImageDescriptor.getMissingImageDescriptor();
                    }
                    return SiriusDiagramEditorPlugin.getInstance().getImage(descriptor);
                }
            }
        }
        return null;
    }

    protected boolean isShowIcon(DDiagramElement vpElement) {
        if (vpElement instanceof MappingBased) {
            DiagramElementMapping vpElementMapping = ((MappingBased) vpElement).getMapping();
            if (vpElementMapping instanceof NodeMapping) {
                return ((NodeMapping) vpElementMapping).isShowIcon();
            }
            if (vpElementMapping instanceof EdgeMapping) {
                return ((EdgeMapping) vpElementMapping).isShowIcon();
            }
            if (vpElementMapping instanceof ContainerMapping) {
                return true;
            }
        }
        return false;
    }

    public Dimension fitToText(DNode node, WrapLabel nodeLabel, DefaultSizeNodeFigure defaultSizeNodeFigure) {
        if (nodeLabel.getFont() != null) {
            String text = node.getName();

            int charHeight = FigureUtilities.getStringExtents("ABCDEF", nodeLabel.getFont()).height + 5;
            int charWidth = FigureUtilities.getTextWidth("ABCDEFGHIJKLMNOPQRSTUVWXYZ", nodeLabel.getFont()) / 26;

            double ratio = charHeight / charWidth;

            int nbLines = (int) (Math.sqrt(text.length()) / ratio) + 1;
            int nbCols = (int) (Math.sqrt(text.length()) * ratio) + 1;

            int longestWord = this.getTheLongestWord(text.split("\\s"));
            nbCols = Math.max(nbCols, longestWord);

            int hHeight = nbLines * charHeight;
            int hWidth = nbCols * charWidth;

            Dimension size = nodeLabel.getPreferredSize(hWidth + nodeLabel.getIconBounds().width +
            		nodeLabel.getIconTextGap(), hHeight).getCopy();

            size.width += 20;
            size.height += 30;

            Insets borderDimension = this.getBorderDimension(node);
            size.width += (borderDimension.left + borderDimension.right);
            size.height += (borderDimension.top + borderDimension.bottom);

            //
            // Square ?
            if (node.getHeight().intValue() == node.getWidth().intValue()) {
                // size.width = Math.max(size.height, size.width);
                // size.height = Math.max(size.height, size.width);
            }

            return size;
        }
        return defaultSizeNodeFigure.getBounds().getSize().getCopy();
    }

    private int getTheLongestWord(String[] strings) {
        int max = -1;
        for (int i = 0; i < strings.length; i++) {
            if (strings[i].length() > max) {
                max = strings[i].length();
            }
        }
        return max;
    }

    /**
     * Return the dimension of the border.
     *
     * @param nodeth
     *            view node.
     * @return the dimension of the border.
     */
    public Insets getBorderDimension(DNode node) {
        return new Insets(0, 0, 0, 0);
    }

}

The easiest way to implement your own configuration is to make it a sub class of SimpleStyleConfiguration.

GMF extension points, CustomStyle and EditPartProvider

CustomStyle is a style that can be applied to a node. Its only property is id that is a string value. By default,
applying a CustomStyle will have a Green Square be displayed. A custom EditPart describing the appearance of the node
must be implemented.

Here is how to apply a CustomStyle:

Next step is to create and record an EditPart to define the way this style will be managed.

An EditPart is a GEF class. It references both a model element and the shape that represents it. GMF adds a layer to GEF
and exposes its own EditPart API. The base type of this API is IGraphicalEditPart (in package
org.eclipse.gmf.runtime.diagram.ui.editparts). Here are the main methods this interface defines:

All Custom Style Edit Part must implement the IStyleEditPart interface. Semantically, a Style Edit Part should not be selectable: it is selected when the Shape or Connector that contains it is selected. Therefore, all Custom Style Edit Part should override the isSelectable() method and return false . If you can, inherit from the abstract class AbstractNotSelectableShapeNodeEditPart that already overrides correctly this method. You should also override the createDefaultEditPolicies to do nothing if you want to let Sirius control your custom node as it is done in the provided Sirius style edit parts: the style part is responsible for the style/figure aspects while its parent part will mainly control the behavior by its policies. Otherwise you will need to properly add policies to override the default GMF policies, add your own specific policies or the Sirius ones.

Here is a sample of a Custom Style Edit Part:

public class InstanceRoleStyleEditPart extends AbstractNotSelectableShapeNodeEditPart implements IStyleEditPart {

    /**
     * the content pane.
     */
    protected IFigure contentPane;

    /**
     * the primary shape.
     */
    protected ImageFigure primaryShape;

    /**
     * Create a new {@link ChangingImageEditPart}.
     *
     * @param view
     *            the view.
     */
    public InstanceRoleStyleEditPart(View view) {
        super(view);
    }

    public DragTracker getDragTracker(Request request) {
        return getParent().getDragTracker(request);
    }

    protected NodeFigure createNodeFigure() {
        NodeFigure figure = createNodePlate();
        figure.setLayoutManager(new XYLayout());
        IFigure shape = createNodeShape();
        figure.add(shape);
        contentPane = setupContentPane(shape);
        return figure;
    }

    private NodeFigure createNodePlate() {
        DefaultSizeNodeFigure result = new AirStyleDefaultSizeNodeFigure(getMapMode().DPtoLP(40), getMapMode().DPtoLP(40));
        return result;
    }

    /**
     * Create the instance role figure.
     *
     * @return the created figure.
     */
    protected ImageFigure createNodeShape() {
        if (primaryShape == null) {
            primaryShape = new ImageFigure();
        }
        return primaryShape;
    }

    /**
     * Return the instance role figure.
     *
     * @return the instance role figure.
     */
    public ImageFigure getPrimaryShape() {
        return primaryShape;
    }

    /**
     * Default implementation treats passed figure as content pane. Respects
     * layout one may have set for generated figure.
     *
     * @param nodeShape
     *            instance of generated figure class
     * @return the figure
     */
    protected IFigure setupContentPane(IFigure nodeShape) {
        return nodeShape; // use nodeShape itself as contentPane
    }

    public IFigure getContentPane() {
        if (contentPane != null) {
            return contentPane;
        }
        return super.getContentPane();
    }

    protected void refreshVisuals() {
        CustomStyle customStyle = (CustomStyle) this.resolveSemanticElement();
        if (customStyle.eContainer() instanceof DNode) {
            this.getPrimaryShape().setImage(SiriusPlugin.getDefault().getBundledImage(((DNode) customStyle.eContainer()).getName()));
        }
    }

    protected void createDefaultEditPolicies() {
        // empty.
    }
}

We now have to notify Sirius that we are providing a new EditPart for nodes having a CustomStyle of that id.
The GMF extension mechanism can be used to achieve this.

plugin.xml:

<extension point="org.eclipse.gmf.runtime.diagram.ui.editpartProviders">
  <editpartProvider class="com.example.diagseq.provider.DiagSeqEditPartProvider">
    <Priority name="High"/>
  </editpartProvider>
</extension>

The provider class ( com.example.diagseq.provider.DiagSeqEditPartProvider) extends the GMF class
org.eclipse.gmf.runtime.diagram.ui.services.editpart.AbstractEditPartProvider. Our custom Edit part will be
provided by overriding getNodeEditPartClass. Here is how this class looks like:

public class DiagSeqEditPartProvider extends AbstractEditPartProvider {
    public DiagSeqEditPartProvider() {
        super();
    }
    
    @Override
    protected Class getNodeEditPartClass(View view) {
        if (view.getElement() instanceof CustomStyle) {
            CustomStyle customStyle = (CustomStyle) view.getElement();
            if (customStyle.getId().equals(DiagSeqConstants.INSTANCE_ROLE_STYLE_ID)) {
                return InstanceRoleStyleEditPart.class;
            }
        }
        return super.getNodeEditPartClass(view);
    }
}

Where DiagSeqConstants.INSTANCE_ROLE_STYLE_ID is the id of our custom style.

In the modeler, the result looks like this:

GMF extension points

GMF exposes all of the necessary API to extend the default behavior of a modeler.

Edit Part Provider

We saw earlier how to provide custom edit parts. Here is how the result looks like in Sirius.

Snapshot of the Sirius meta-model:

The types DNode, DNodeContainer, DNodeList, DNodeListElement, etc. have their own edit parts:

Edit Policy Provider

Edit Policies are objects responsible for the handling of user actions. There are Edit Policies for different behaviors:

As with edit parts, it is necessary to write an Edit Policy Provider as well as the Edit Policy that is to be provided. The
Edit Policy Provider must implement the IEditPolicyProvider interface:

Here is an example of an Edit Policy Provider

/**
 * Provides Edit Policy for Note Attachment.
 */
public class AirNoteAttachmentEditPolicyProvider implements IEditPolicyProvider {

    /** the property change support. */
    private List listeners;

    /**
     * Create a new {@link AirNoteAttachmentEditPolicyProvider}.
     */
    public AirNoteAttachmentEditPolicyProvider() {
        this.listeners = new ArrayList(2);
    }

    public void createEditPolicies(EditPart editPart) {
        if (editPart instanceof NoteAttachmentEditPart) {
            editPart.installEditPolicy(EditPolicy.CONNECTION_ROLE, new AirNoteAttachmentEditPolicy());
        }
    }

    public void addProviderChangeListener(IProviderChangeListener listener) {
        this.listeners.add(listener);
    }

    public boolean provides(IOperation operation) {
        if (operation instanceof CreateEditPoliciesOperation) {
            CreateEditPoliciesOperation castedOperation = (CreateEditPoliciesOperation) operation;
            EditPart editPart = castedOperation.getEditPart();
            Object model = editPart.getModel();
            if (model instanceof View) {
                View view = (View) model;
                if (view.getDiagram() != null && view.getDiagram().getElement() != null
                        && view.getDiagram().getElement().eClass().getEPackage().getNsURI().equals(ViewpointPackage.eINSTANCE.getNsURI())) {
                    if ("NoteAttachment".equals(view.getType())) {
                        return true;
                    }
                }
            }
        }
        return false;
    }

    public void removeProviderChangeListener(IProviderChangeListener listener) {
        this.listeners.remove(listener);
    }

    /**
     * Fire a {@link ProviderChangeEvent}.
     */
    protected void fireProviderChanged() {
        ProviderChangeEvent event = new ProviderChangeEvent(this);
        Iterator iterListener = this.listeners.iterator();
        while (iterListener.hasNext()) {
            IProviderChangeListener listener = (IProviderChangeListener) iterListener.next();
            listener.providerChanged(event);
        }
    }

}

More flexibility can be achieved by writing an abstract provider:

public abstract class AbstractEditPolicyProvider implements IEditPolicyProvider {

    /** All listeners. */
    private List listeners = new ArrayList(1);

    public void addProviderChangeListener(IProviderChangeListener listener) {
        this.listeners.add(listener);
    }

    public void removeProviderChangeListener(IProviderChangeListener listener) {
        this.listeners.remove(listener);
    }

    protected void fireProviderChanged() {
        ProviderChangeEvent event = new ProviderChangeEvent(this);
        Iterator iterListener = this.listeners.iterator();
        while (iterListener.hasNext()) {
            IProviderChangeListener listener = (IProviderChangeListener) iterListener.next();
            listener.providerChanged(event);
        }
    }
}

There only remains to override the methods provides() and createEditPolicies() in this provider’s implementations.