Generate graphical Visualizations for textual DSLs

The advantage of using a textual notation for models is that no special editors are required to create and edit these models. Furthermore, the models can also be diffed and merged using standard tools. Such textual notations are particularly suitable for expert tools. Graphical notation, on the other hand, illustrate complex relations and are often easier to understand. The combination of both notations makes optimum use of their particular benefits.

In this post I will demonstrate how we can achieve this goal by enhancing the textual Xtext editors with graphical visualizations. The visualization will be shown in a view within the Eclipse IDE. Thus, we will use the SWT-based Zest toolkit for rendering.


Let’s start with any arbitrary Xtext DSL. I will use the entity-relationship DSL that was also used in post Post 05/25.

Make sure you have the Zest plugins installed. This is not the case if you are using a standard eclipse distribution. They can be downloaded and installed from the update site http://download.eclipse.org/tools/gef/updates/releases/.

Create a new plugin project and open the plugin.xml. We need to add dependencies to the following plugins:

  • Zest: org.eclipse.zest.layout, org.eclipse.zest.core
  • Xtext: org.eclipse.xtext.ui
  • Eclipse Core Components: org.eclipse.ui.forms, org.eclipse.ui.console
  • JDT: org.eclipse.jdt.ui

The visualization will be displayed in a new view within the Eclipse IDE. Thus, we have to register a “view” to the extension point org.eclipse.ui.views.

This is the resulting plugin.xml:

<?xml version="1.0" encoding="UTF-8"?>
<?eclipse version="3.4"?>
<plugin>
   <extension point="org.eclipse.ui.views">
      <category
            id="org.xtext.example.annotation.ui.visualization.views.category" 
            name="Model Visualizations" />   
      <view
            allowMultiple="false"
            category="org.xtext.example.annotation.ui.visualization.views.category"
            class="org.xtext.example.annotation.ui.visualization.bindings.RelationshipVisualizationView"
            icon="icons/obj16/VisualizationViewIcon.png"
            id="org.xtext.example.annotation.ui.visualization.views.bindings"
            name="Visualization Example"
            restorable="true" />
   </extension>
</plugin>

This is how your MANIFEST.MF might look like:

Manifest-Version: 1.0
Bundle-ManifestVersion: 2
Bundle-Name: Visualization Example
Bundle-SymbolicName: org.xtext.example.annotation.visualization;singleton:=true
Bundle-Version: 1.0.0
Bundle-Activator: org.xtext.example.annotation.ui.visualization.Activator
Bundle-Vendor: XTEXTerience
Require-Bundle: org.eclipse.zest.layouts;bundle-version="1.1.0",
 org.eclipse.xtext.ui;bundle-version="1.0.1",
 org.eclipse.zest.core;bundle-version="1.2.0",
 org.eclipse.ui.forms;bundle-version="3.5.0",
 org.eclipse.ui.console,
 org.eclipse.jdt.ui
Bundle-RequiredExecutionEnvironment: JavaSE-1.6
Bundle-ActivationPolicy: lazy

We will use Eclipse selection service to track which model is currently opened. This requires to implement ISelectionListener and register this implementation at the Selection Service. The registration is done in createPartControl-method immediately after it has created the view’s controls. We also need to unregister the view in the dispose-method called when it is destroyed.:

public class RelationshipVisualizationView extends ViewPart implements ..., ISelectionListener {
	...

	public void selectionChanged(
		IWorkbenchPart sourcepart, ISelection selection) {

		// This method is called whenever the selection changes
		...
	}

	public void createPartControl(Composite parent) {
		...
		// Register selection service listener
		getSite().getWorkbenchWindow().getSelectionService().
			addPostSelectionListener(this);
	}

	public void dispose() {
		// Unregister selection service listener
		getSite().getWorkbenchWindow().getSelectionService().
			removePostSelectionListener(this);
		...
	}
}

In the selectionChanged-method the selection is analysed. If it is a selection within a Xtext editor its contents are parsed to an AST. For this purpose we will use the method described here. Finally, the model element at the caret position is determined and used as input to render the visualization.

public void selectionChanged(
	IWorkbenchPart sourcepart, ISelection selection) {

	// we ignore our own selections
	if (sourcepart != this) {
	if (
		!selection.isEmpty() && 
		selection instanceof ITextSelection && 
		sourcepart instanceof XtextEditor) {

		final ITextSelection textSel = (ITextSelection) selection;
		final XtextEditor editor = (XtextEditor)sourcepart;
		final IXtextDocument document = editor.getDocument();
    		
		// determine the model element at the offset
		// Access to the underlying resource in the Xtext editor need to
		// be encapsulated by an IUnitOfWork
		document.readOnly(new IUnitOfWork.Void<XtextResource>() {
			public void process(XtextResource resource) throws Exception {
				// parse the whole resource
				IParseResult parseResult = resource.getParseResult();
				if(parseResult == null) return;
					
				// Get the root of the parsing result
				CompositeNode rootNode = parseResult.getRootNode();
					
				// Get the parsing result around the current offset
				int offset = textSel.getOffset();
				AbstractNode node = ParseTreeUtil.
				 	getCurrentOrFollowingNodeByOffset(rootNode, offset);
				EObject object = NodeUtil.getNearestSemanticObject(node);
					
				visualizationForm.browseTo(object);
			}
    		});
		} else {
			visualizationForm.browseTo(null);
		}
	}
}

Zest’s Graph Viewer work the same view as all JFace viewers. Therefore we need to provide a content provider and a label provider. The concrete implementation of these providers is straightforward and is highly depends of your usecase.

Besides for org.eclipse.jface.viewers.ILabelProvider such a label provider also needs to implement org.eclipse.zest.core.viewers.IEntityStyleProvider and org.eclipse.zest.core.viewers.IConnectionStyleProvider. The following code shows the relevant part of the implementation I used to render the visualization of the Entity-Relationship DSL:

public class RelationshipLabelProvider implements 
	ILabelProvider, IConnectionStyleProvider, IEntityStyleProvider {

	...

	public Image getImage(Object element) {
		// Get the 
		if (element instanceof Entity) {
			return VisualizationImageManager.get(
				VisualizationImageManager.IMG_ENTITY);
		} else {
			return null;
		}
	}

	public String getText(Object element) {
		
		if (element instanceof EntityConnectionData) { // = Relations
			EntityConnectionData connection = 
				(EntityConnectionData) element;
			
			// Retrieve the original relationships model element from 
			// content provider 
			IContentProvider contentProvider = viewer.getContentProvider();
			if (contentProvider instanceof RelationshipContentProvider) {
				RelationshipContentProvider bindingGraphContentProvider =
					(RelationshipContentProvider)contentProvider;
				List<String> descriptions = 
					bindingGraphContentProvider.getRelationDescription(
						connection.source, connection.dest);
				
				// Append all relationsship descriptions
				StringBuilder sb = new StringBuilder();
				boolean first = true;
				for (String description : descriptions) {
					if (first) {
						first = false;
					} else {
						sb.append("\n");
					}
					sb.append(description);
				}
				return sb.toString();
			} else {
				throw new RuntimeException(
					"The content provider need to be an instance of " +
 					RelationshipContentProvider.class.getCanonicalName());
			}
			
		} else if (element instanceof Entity) {// Entities
			Entity entity = (Entity)element;
			return entity.getName();
		} else {
			return null;
		}
	}
	...
}

The content provider needs to implement org.eclipse.zest.core.viewers.IGraphEntityContentProvider. The following example can follow all incoming relations as well as the outgoing relations of the selected entity. For this purpose it iterates all entities and relations in the model. At the same time it builds maps of the interrelations. Based on this information then the visible elements are determined.

public class RelationshipContentProvider implements 
	IGraphEntityContentProvider {

	private boolean showIncomingRelations;
	private boolean showOutgoingRelations;
	
	private List<Relation> relations;
	private Set<Entity> entities;
	
	private EObject input;
	
	/**
	 * A utility method called from the label provider 
	 * that is used to get the description of a relation 
	 * @param from The source entity of the relation
	 * @param to The target entity of the relation
	 * @return The description of the relation connecting 
	 * the source entity to the target entity
	 */
	public List<String> getRelationDescription(Object from, Object to) {
		List<String> result = new ArrayList<String>();
		
		for (Relation relation : relations) {
			if (relation.getLeft() == from && relation.getRight() == to) {
				if (relation.getDescription() != null) {
					result.add(relation.getDescription().getText());
				}
			}
		}
		return result;
	}
	
	/**
	 * Constructor
	 * @param showIncomingRelations whether incoming relations are shown
	 * @param showOutgoingRelations whether outgoing relations are shown
	 */
	public RelationshipContentProvider(
		boolean showIncomingRelations, boolean showOutgoingRelations) {

		super();
		this.showIncomingRelations = showIncomingRelations;
		this.showOutgoingRelations = showOutgoingRelations;
		
		relations = new ArrayList<Relation>();
		entities = new HashSet<Entity>();
		
		input = null;
	}
	
	public Object[] getConnectedTo(Object o) {
		if (o instanceof Entity) {
			Entity entity = (Entity)o;
			List<Entity> result = new ArrayList<Entity>();
			for (Relation relation : relations) {
				if (relation.getLeft() == entity) {
					result.add(relation.getRight());
				}
			}
			return result.toArray();
		}
		return null;
	}

	public Object[] getElements(Object inputElement) {
		List<EObject> result = new ArrayList<EObject>();
		result.addAll(entities);
		return result.toArray();
	}

	public void dispose() {
		input = null;
		entities.clear();
		relations.clear();
	}
	
	public void inputChanged(
		Viewer viewer, Object oldInput, Object newInput) {

		if (newInput == null) {
			input = null;
			
			rebuildRelations();
		} else {
			if (newInput instanceof Entity) {
				input = (EObject) newInput;
				rebuildRelations();
			} else {
				throw new RuntimeException(
					"Input element not supported! Need to be " + 
					Entity.class.getCanonicalName());
			}
		}
	}

	/**
	 * Setter, shows or hides incoming relations
	 * @param enable
	 */
	public void setShowIncomingRelations(boolean enable) {
		this.showIncomingRelations = enable;
		rebuildRelations();
	}

	/**
	 * Setter, shows or hides outgoing relations
	 * @param enable
	 */
	public void setShowOutgoingRelations(boolean enable) {
		this.showOutgoingRelations = enable;
		rebuildRelations();
	}

	
	/**
	 * Used internally to build up all relations and caches. 
	 * Must be called after the visualization options have been changed
	 */
	private void rebuildRelations() {
		entities.clear();
		relations.clear();
		
		if (input != null) {
			// Some temporary buffers
			Set<Entity> allEntities = new HashSet<Entity>();
			Map<Entity, Set<Relation>> incomingRelations = 
				new HashMap<Entity, Set<Relation>>();
			Map<Entity, Set<Relation>> outgoingRelations = 
				new HashMap<Entity, Set<Relation>>();
			
			if (input instanceof Entity) {
				Entity entity = (Entity)input;
				
				// Iterate over all elements in model
				List<EObject> contents = entity.eResource().getContents();
				for (EObject rootContent : contents) {
					if (rootContent instanceof Model) {
						Model model = (Model)rootContent;
						for (Elements element : model.getContents()) {
							if (element instanceof Entity) {
								Entity e = (Entity)element;
								allEntities.add(e);
								
							} else if (element instanceof Relation) {
								Relation r = (Relation)element;
								
								Set<Relation> incomingRelationsForEntity = 
									incomingRelations.get(r.getRight());
								if (incomingRelationsForEntity == null) {
									incomingRelationsForEntity = 
										new HashSet<Relation>();
									incomingRelations.put(
										r.getRight(),
										incomingRelationsForEntity);
								}
								incomingRelationsForEntity.add(r);
								
								Set<Relation> outgoingRelationsForEntity = 
									outgoingRelations.get(r.getLeft());
								if (outgoingRelationsForEntity == null) {
									outgoingRelationsForEntity = 
										new HashSet<Relation>();
									outgoingRelations.put(
										r.getLeft(), 
										outgoingRelationsForEntity);
								}
								outgoingRelationsForEntity.add(r);
							}
						}
					}
				}

				// Then build the visual representation of the part of 
				// the network that is to be shown
				if (showIncomingRelations) {
					retrieveEntitiesAndRelations(
						entity, incomingRelations, true);
				}
				
				if (showOutgoingRelations) {
					retrieveEntitiesAndRelations(
						entity, outgoingRelations, false);
				}
			}
		}
	}
	
	private void retrieveEntitiesAndRelations(
		Entity root, 
		Map<Entity, Set<Relation>> allRelations, 
		boolean backwards) {

		Set<Entity> visitedEntities = new HashSet<Entity>();
		visitedEntities.clear();
		internalRetrieveEntitiesAndRelations(root, visitedEntities, allRelations, backwards);
	}

	private void internalRetrieveEntitiesAndRelations(
		Entity root, 
		Set<Entity> visitedEntities, 
		Map<Entity, Set<Relation>> allRelations, 
		boolean backwards) {

		entities.add(root);
		Set<Relation> relationsForEntity = allRelations.get(root);
		if (relationsForEntity != null) {
			for (Relation relationForEntity : relationsForEntity) {
				relations.add(relationForEntity);
			
				Entity next = null;
				if (backwards) {
					next = relationForEntity.getLeft();
				} else {
					next = relationForEntity.getRight();
				}
				
				// Recursive call if this element has not been in 
				// focus before
				if (!visitedEntities.contains(next)) {
					visitedEntities.add(next);
					internalRetrieveEntitiesAndRelations(
						next, visitedEntities, allRelations, backwards);
				} 				
			}
		}
	}
}

The following snippet shows how the view instantiates the graph viewer and registers its label and content providers.

...
viewer = new org.eclipse.zest.core.viewers.GraphViewer(section, SWT.NONE);
...
viewer.setContentProvider(new RelationshipContentProvider(...));
viewer.setLabelProvider(new RelationshipLabelProvider(...););
viewer.setInput(null);
viewer.setConnectionStyle(ZestStyles.CONNECTIONS_DIRECTED);
viewer.setLayoutAlgorithm(
	new CompositeLayoutAlgorithm(
		LayoutStyles.NO_LAYOUT_NODE_RESIZING, 
		new LayoutAlgorithm[] { 
			new DirectedGraphLayoutAlgorithm(
				LayoutStyles.NO_LAYOUT_NODE_RESIZING), 
			new HorizontalShift(LayoutStyles.NO_LAYOUT_NODE_RESIZING) 
		}
	)
);
...

The sources of this post can be downloaded here (Xtext 1.0) or here (Xtext 2.3.1).

The screenshot shows the running Xtext editor together with the corresponding visualization.

Advertisements
    • Frank
    • March 21st, 2012

    Hi. Do know the changes from Zest 1.0.1 to 2.0.0? I tried your sources but i get tons of errors. Regards Frank

    • Hi Frank.
      Indeed, the sources were built with Zest 1.0 and I haven’t found the time to migrate them to a newer version. If you will do so, you might consider providing me the sources so we can share them with other on the download page.
      Cheers
      Simon

  1. Hi Simon,
    Do you know how to create custom shape for the GraphNode in Zest? In default, it just show the label text inside rounded rectangle. But I want to create custom shape, like table which in its cell there are text or anything.

    • I also had some troubles working with custom shapes. What worked well was deriving from basic figure classes like “org.eclipse.draw2d.Ellipse” and override the fillShape function to do custom drawing.
      Another possibility is to build up a hierarchy of sub figures in the constructor. See the example below that draws a state as known from statemachines:

      public class StateFigure extends RoundedRectangle {

      public StateFigure(String name, boolean hasChildren, String startTransitionText, String entryTransitionText, String exitTransitionText, String doTransitionText, List localTransitionTexts) {

      this.setSize(-1, -1);

      ToolbarLayout layoutThis = new ToolbarLayout(false);
      layoutThis.setVertical(true);
      layoutThis.setSpacing(5);
      layoutThis.setStretchMinorAxis(true);
      layoutThis.setMinorAlignment(ToolbarLayout.ALIGN_TOPLEFT);
      this.setLayoutManager(layoutThis);

      this.stateNameLabel = new Label(name);
      stateNameLabel.setTextAlignment(PositionConstants.CENTER | PositionConstants.MIDDLE);
      this.add(this.stateNameLabel);

      if (hasChildren) {
      stateNameLabel.setIcon(VisualizationImages.DESC_DOWNWARDS.createImage());
      } else {
      //rect.setLineWidth(1);
      }

      if (
      startTransitionText != null ||
      entryTransitionText != null ||
      exitTransitionText != null ||
      doTransitionText != null ||
      !localTransitionTexts.isEmpty()
      ) {

      RectangleFigure rect = new RectangleFigure();

      this.add(rect);
      GridLayout l = new GridLayout(1, false);
      rect.setLayoutManager(l);

      if (startTransitionText != null) {
      Label startTransitionLabel = new Label(startTransitionText);
      rect.add(startTransitionLabel);
      }
      if (entryTransitionText != null) {
      Label entryTransitionLabel = new Label(entryTransitionText);
      rect.add(entryTransitionLabel);
      }
      if (exitTransitionText != null) {
      Label exitTransitionLabel = new Label(exitTransitionText);
      rect.add(exitTransitionLabel);
      }
      if (doTransitionText != null) {
      Label doTransitionLabel = new Label(doTransitionText);
      rect.add(doTransitionLabel);
      }
      for (String localTransitionText : localTransitionTexts) {
      Label localTransitionLabel = new Label(localTransitionText);
      rect.add(localTransitionLabel);
      }
      }
      }

      }

      • Thanks Simon, I’ll try your code 😀
        Eventually, I managed to create custom Node by extending CGraphNode class in Zest. By doing this, I have to override getFigure method and return the custom Figure by implementing it using draw2d. I have been trying it all day to create table figure, but somehow the result is not very suitable with what I want, perhaps because I don’t know much about draw2d. Do you have any tutorial or article that explain deeply about draw2d?

      • The only thing I found about Draw2D is about its usage in conjunction with GEF: http://www.eclipse.org/articles/Article-GEF-Draw2d/GEF-Draw2d.html

  2. Thanks to the anonymous guy that migrated the sources to Xtext 2.3.1. I uploaded them to google code and updated the post accordingly.

  1. No trackbacks yet.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: