Skip to content

Visualising Xtext Models with Picto

This article shows how Picto can be used to produce graphical views from Xtext-based models. To demonstrate the Picto/Xtext integration, we use the Entity DSL showcased in the 15' Xtext tutorial.

Setup

  • Import the projects below from the examples folder of the Epsilon Git repository
    • org.eclipse.epsilon.examples.picto.xtext.domainmodel
    • org.eclipse.epsilon.examples.picto.xtext.domainmodel.ide
    • org.eclipse.epsilon.examples.picto.xtext.domainmodel.ui
    • org.eclipse.epsilon.examples.picto.xtext.domainmodel.picto
  • Right-click on Domainmodel.xtext and select Run AsGenerate Xtext Artefacts
  • Run a nested Eclipse instance.
  • In the nested Eclipse instance workspace, create a new file named blog.dmodel with the content below:
datatype String

entity Blog {
    title: String
    many posts: Post
}

entity HasAuthor {
    author: String
}

entity Post extends HasAuthor {
    title: String
    content: String
    many comments: Comment
}

entity Comment extends HasAuthor {
    content: String
    many responses: Comment
}
  • Open the Picto view from the Window → Show View menu.
  • Go through the produced graphical views as shown in the image above.

The Picto-Xtext Integration Plugin Project

We now dive into the org.eclipse.epsilon.examples.picto.xtext.domainmodel.picto project which contains the Picto-Xtext integration code (the rest of the projects are standard Xtext projects).

The DmodelPictoSource class

This class extends Picto's built-in EglPictoSource class and produces graphical views in Picto from *.dmodel files edited in an Xtext-based editor. In particular:

  • The supportsEditorType method specifies that this class contributes visualisation capabilities to Xtext-based editors, the title of which ends with .dmodel
  • The getRenderingMetadata method specifies the EGL transformation that produces the graphical views every time an editor of interest is opened, activated or saved.
  • The getFile and getResource methods extract an IFile and an EMF Resource from the editor of interest and should be reusable without changes for other Xtext-based languages too.
  • The showElement method reveals and highlights the element with the specified id in the Xtext editor, enabling navigation back to the source model of the view.
package org.eclipse.epsilon.examples.picto.xtext.domainmodel.picto;

import org.eclipse.core.resources.IFile;
import org.eclipse.emf.ecore.resource.Resource;
import org.eclipse.epsilon.picto.dom.Picto;
import org.eclipse.epsilon.picto.dom.PictoFactory;
import org.eclipse.epsilon.picto.source.EglPictoSource;
import org.eclipse.jface.text.source.ISourceViewer;
import org.eclipse.ui.IEditorInput;
import org.eclipse.ui.IEditorPart;
import org.eclipse.ui.IFileEditorInput;
import org.eclipse.ui.PlatformUI;
import org.eclipse.xtext.nodemodel.ICompositeNode;
import org.eclipse.xtext.nodemodel.util.NodeModelUtils;
import org.eclipse.xtext.resource.XtextResource;
import org.eclipse.xtext.ui.editor.XtextEditor;
import org.eclipse.xtext.ui.workspace.WorkspaceLockAccess.Result;
import org.eclipse.xtext.util.concurrent.IUnitOfWork;

public class DmodelPictoSource extends EglPictoSource {

    @Override
    protected Picto getRenderingMetadata(IEditorPart editorPart) {
        Picto metadata = PictoFactory.eINSTANCE.createPicto();
        metadata.setTransformation("platform:/plugin/org.eclipse.epsilon.examples.picto.xtext.domainmodel.picto/dmodel.egx");
        return metadata;
    }

    @Override
    protected Resource getResource(IEditorPart editorPart) {
        XtextEditor editor = (XtextEditor) editorPart;
        final XtextResourceHolder holder = new XtextResourceHolder();
        editor.getDocument().readOnly(new IUnitOfWork<Result, XtextResource>() {
            public Result exec(XtextResource state) throws Exception {
                holder.setResource(state);
                return null;
            };
        });

        return holder.getResource();
    }

    @Override
    protected IFile getFile(IEditorPart editorPart) {
        IEditorInput editorInput = ((XtextEditor) editorPart).getEditorInput();
        if (editorInput instanceof IFileEditorInput) {
            return ((IFileEditorInput) editorInput).getFile();
        }
        return null;
    }

    @Override
    protected boolean supportsEditorType(IEditorPart editorPart) {
        return editorPart instanceof XtextEditor && 
            editorPart.getTitle().endsWith(".dmodel");
    }

    @Override
    public void showElement(String id, String uri, IEditorPart editor) {
        ICompositeNode node = NodeModelUtils.getNode(getResource(editor).getEObject(id));
        if (node != null) {
            ISourceViewer textViewer = ((XtextEditor) editor).getInternalSourceViewer();
            int offset = node.getOffset();
            int length = node.getLength();
            textViewer.setRangeIndication(offset, length, true);
            textViewer.revealRange(offset, length);
            textViewer.setSelectedRange(offset, length);
            PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage().activate(editor);
        }
    }

}

Picto is made aware of this class through the org.eclipse.epsilon.picto.pictoSource extension in the project's plugin.xml.

<?xml version="1.0" encoding="UTF-8"?>
<?eclipse version="3.4"?>
<plugin>
   <extension
         point="org.eclipse.epsilon.picto.pictoSource">
      <pictoSource
            class="org.eclipse.epsilon.examples.picto.xtext.domainmodel.picto.DmodelPictoSource">
      </pictoSource>
   </extension>

</plugin>

The Visualisation Transformation

The visualisation transformation has been implemented using Epsilon's EGL model-to-text transformation language and produces Graphviz-based class diagrams from the active entity model. The transformation consists of the dmodel.egx coordination rules, and the entity2graphviz.egl template shown below. The transformation produces one view for each entity in the model, which consists of the entity itself, any super/sub-types it may have, as well as other entities that it is related to.

rule Entity2Graphviz
    transform e : Entity {

    template : "entity2graphviz.egl"

    parameters : Map{
        "path" = Sequence{"Model", e.name},
        "icon" = "diagram-ffffff",
        "format" = "graphviz-dot"
    }

}
digraph G {
    graph[splines=ortho]
    node[fontname=Arial, fontsize=10, shape=record]
    edge[fontname=Arial, fontsize=10]

[%var entities = getVisibleEntities();%]

[%for (e in entities){%]
    [%=e.getNodeName()%][shape=none, margin=0, label=<[%=e.getLabel()%]>]
[%}%]

[%for (f in e.features.flatten().select(f|f.type.isTypeOf(Entity) and f.type <> e)){%]
    [%=f.eContainer.getNodeName()%]->[%=f.type.getNodeName()%][arrowhead=[%=f.getArrow()%],arrowtail=none,tooltip="[%=f.name%]"];
[%}%]

[%if (e.superType.isDefined()){%]
    [%=e.superType.getNodeName()%]->[%=e.getNodeName()%][arrowhead=none,arrowtail=empty,dir=back];
[%}%]

[%for (s in Entity.all.select(en|en.superType = e)){%]
    [%=e.getNodeName()%]->[%=s.getNodeName()%][arrowhead=none,arrowtail=empty,dir=back];
[%}%]

}

[%
operation Entity getLabel() {

    var onClick = "top.showView(['Model','" + self.name + "'])";

    if (self == e) {
        onClick = "top.showElement('" + self.id + "', '" + self.eResource.uri + "')";
    }

    var label = "<table cellspacing='0' cellborder='0' cellpadding='1' bgcolor='" + self.getColour() + "'>";
    label += "<tr><td sides='B' colspan='2' border='1' cellpadding='0'>" + 
        "<table border='0' cellspacing='0' cellborder='0'>" + 
        "<tr><td align='right' valign='middle'><img src='" + self.getIcon()+ "'></img></td>" + 
        "<td align='left' valign='middle' href=\"javascript:" + onClick + "\" tooltip='Go'>" + self.name + " </td></tr></table></td></tr>";

    label += "<tr><td></td><td></td></tr>";

    for (f in self.features.sortBy(a|a.name.toLowerCase())) {
        label += "<tr>";
        label += "<td><img src='" + f.getIcon() + "'></img></td><td align='left'>" + f.getLabel() + "</td>";
        label += "</tr>";
    }


    if (self.features.isEmpty()){
        label += "<tr>";
        label += "<td> </td><td> </td>";
        label += "</tr>";
    }

    label += "</table>";
    return label;
}

operation Entity getIcon() {
    return getImage("icons/entity.gif");
}

operation Feature getLabel() {
    return self.name + " : " + self.type?.name + (self.many ? "["+"*"+"]" : "") ;
}

operation Feature getIcon() {
    return getImage("icons/attribute.gif");
}

operation Entity getNodeName() {
    return "_Entity" + Entity.all.indexOf(self);
}

operation Feature getArrow() {
    if (self.many) {
        return "crow";
    }
    else {
        return "open";
    }
}

operation getVisibleEntities() {    
    var visibleEntities : Set;
    visibleEntities.add(e);
    visibleEntities.addAll(e.features.select(f|f.type.isTypeOf(Entity)).collect(f|f.type));
    if (e.superType.isDefined()) visibleEntities.add(e.superType);
    visibleEntities.addAll(Entity.all.select(en|en.superType = e));
    return visibleEntities;
}

operation Entity getColour() {
    if (self == e) return "#fff2d2";
    else return "#fffcdc";
}
%]

Interactive Diagrams

As shown below, you can navigate between diagrams and back to the Xtext editor using Picto's built-in showElement and showView JavaScript functions.

Lazy Execution

Since Picto executes EGL transformations lazily when the entity model is saved, only the view that is currently visible is regenerated immediately, which is useful when working with large models.