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 repositoryorg.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 selectRun As
→Generate 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
andgetResource
methods extract anIFile
and an EMFResource
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 specifiedid
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.