Adding additional Tools (aka Views)

This document describes how to build a new Tool (For Eclipse
aspects of Tool development: how to add a new tool to a language and the menu

endeavour that is surprisingly non-trivial!

The Outline Tool itself


one remember the ToolSynchronizer. The ToolSynchronizer updates the
platform's infrastructure for event distribution. We use it for getting notified

private Project project;                    
private MessageBusConnection messageBusConn;
private ToolSynchronizer synchronizer;      

dispose and getComponent. Here is the code (with comments, please
read them!):

init(project)->void {
  this.project = project; 

dispose(project)->void { 
  // disconnect from message bus -- connected in getComponent() 
  if ( this.messageBusConn != null ) this.messageBusConn.disconnect(); 

getComponent()->JComponent { 
  // create a new JTree with a custom cell renderer (for rendering custom icons) 
  JTree tree = new JTree(new Object[0]); 
  tree.setCellRenderer(new OutlineTreeRenderer(tree.getCellRenderer())); 
  // create a new synchronizer. It needs the tree to notify it of updates. 
  this.synchronizer = new ToolSynchronizer(tree); 
  // connect to the message bus. The synchronizer receives editor change events 
  // and pushes them on to the tree. The synchronizer implements 
  // FileEditorChangeListener to be able to make this work
  this.messageBusConn = this.project.getMessageBus().connect(); 
  // we finally return a ScrollPane with the tree in it 
  return new JScrollPane(tree); 

An Action to Open the Tool

context includes the currently opened project as well as the currently opened
file editor.

  action context parameters ( always visible = false )                                                                                          
    Project project key: PROJECT required                                                                                                       
    FileEditor editor key: FILE_EDITOR required                                                                                                 

itseld. It simply stores the editor that's passed in.

execute(event)->void { 
  tool<Outline> outline = this.project.tool<Outline>; 

An Action Group


group OutlineGroup                     
is popup: false                        
   add to Tools at position customTools

Managing the Tool Lifecycle

EditorActivationListener tracks which of the potentially many open editors
below). The ModelLifecycleListener tracks lifecycle events for the model

Editor Activation and Focus

The EditorActivationListener tracks which of the potentially many open

this.messageBusConn = this.project.getMessageBus().connect(); 

tree selection listener that listens to selection changes made by the user
in the tree itself.

this.outlineSelectionListener = new OutlineSelectionListener(this); 


modelLifecycleListener = new ModelLifecycleListener(tree); 
eventsCollector = new ModelChangeListener(tree);

The EditorActivationListener implements FileEditorManagerListener, so
it has to implement the following three methods:

void fileOpened(FileEditorManager p0, VirtualFile p1); 
void fileClosed(FileEditorManager p0, VirtualFile p1); 
void selectionChanged(FileEditorManagerEvent p0);


public void selectionChanged(FileEditorManagerEvent event) { 
  // read action is required since we access the model 
  read action { 
    FileEditor oldEditor = event.getOldEditor(); 
    // grab the old editor and clean it up if there is one 
    if (oldEditor != null) { 
    // call a helper method that sets up the new editor 

The cleanupOldEditor method removes existing listeners from the old, now
inactive editor:

private void cleanupOldEditor(FileEditor oldEditor) { 
  // Downcast from IDEA level to MPS specifics and 
  // grab the NodeEditor component 
  IEditor oldNodeEditor = ((MPSFileNodeEditor) oldEditor).getNodeEditor(); 
  if (oldNodeEditor != null && this.editorSelectionListener != null) { 
    // remove the selection listener from the old editor 
    // grab the descriptor of the model edited by the old editor 
    // and remove the model listener (cleanup!) 
    SModelDescriptor descriptor = oldNodeEditor.getEditorContext().getModel().
    // remove the model edited by the old editor from the events collector 
    // ...we are not interested anymore. 

underlying model. Notice how we use SNodePointer whenever we keep
avoid memory leaks!

public void newEditorActivated(FileEditor fileEditor) { 
  if (fileEditor != null) { 
    // remember the current editor 
    this.currentEditor = ((MPSFileNodeEditor) fileEditor); 
    // grab the root node of that new editor... 
    SNode rootNode = this.currentEditor.getNodeEditor().
    // ...wrap it in an SNodePointer... 
    SNodePointer treeRoot = new SNodePointer(rootNode); 
    // and create a new outline tree model 
    OutlineModel model = new OutlineModel(treeRoot); 
    // create a new selection listener and hook it up 
    // with the newly selected editor 
    this.editorSelectionListener = new EditorSelectionListener(tree, 
    SelectionManager selectionManager = this.currentEditor.getNodeEditor().
    // This is needed to detect reloading of a model 
    ((MPSFileNodeEditor) fileEditor).getNodeEditor().
  } else { 
    tree.setModel(new OutlineModel(null)); 

Tracking the Model Lifecycle

The ModelLifecycleListener extends the SModelAdapter class provided
modelReplaced method. It is called whenever the currenly held model is

of subseqent model replacement events.

public void modelReplaced(SModelDescriptor descriptor) { 
  tree.setModel(new OutlineModel(((OutlineModel) tree.getModel()).getRealRoot())); 

Synchronizing Node Selections

Tracking Editor Selection


The class extends MPS' SingularSelectionListenerAdapter because we are
only interested in single node selections. We overwrite the
selectionChangedTo method in the following way:

protected void selectionChangedTo(EditorComponent component, 
                                  SingularSelection selection) { 
  // do nothing is disabled -- prevents cyclic, never ending updates 
  if (disabled) { return; } 
  // read action, because we access the model 
  read action { 
    // gran the current selection 
    SNode selectedNode = selection.getSelectedNodes().get(0); 
    // ... only if it has changed... 
    if (selectedNode != lastSelection) { 
      lastSelection = selectedNode; 
      // disable the tree selection listener, once again to prevent cyclic 
      // never ending updates 
      // select the actual node in the tree 
      tree.setSelectionPath(((OutlineModel) tree.getModel()).

Tracking Changes in the Model Structure


class that extends SModelEventVisitorAdapter to visit the events and react
to those that we are interested in.

protected void eventsHappened(List<SModelEvent> list) { 
  foreach evt in list { 
    evt.accept(new ModelChangeVisitor()); 

The ModelChangeVisitor inner class, which acts as the visitor to notify
the tree, overrides visitPropertyEvent to find out about nodes whose
of the tree model.

public void visitPropertyEvent(SModelPropertyEvent event) { 
  OutlineModel outlineModel = ((OutlineModel) tree.getModel()); 
  foreach l in outlineModel.getListeners() { 
    l.treeNodesChanged(new TreeModelEvent(this, 

It also overwrites visitChildEvent to get notified of child

public void visitChildEvent(SModelChildEvent event) { 
  // grab the model 
  OutlineModel outlineModel = ((OutlineModel) tree.getModel()); 
  // we need the following arrays later for the JTree API 
  Object[] child = new Object[]{event.getChild()}; 
  int[] childIndex = new int[]{event.getChildIndex()}; 
  // we create a tree path to the parent notify all listeners 
  // of adding or removing children 
  TreePath path = outlineModel.getPathTo(event.getParent()); 
  if (path == null) { return; } 
  // notify the tree model's listeners about what happened 
  foreach l in outlineModel.getListeners() { 
    if (event.isAdded()) { 
      l.treeNodesInserted(new TreeModelEvent(this, path, childIndex, child)); 
    } else if (event.isRemoved()) { 
      l.treeNodesRemoved(new TreeModelEvent(this, path, childIndex, child)); 

The way back: Tracking Tree Selection

Tracking a JTree's selection happens by implementing Swing's
TreeSelectionListener and overwriting it's valueChanged method the
following way:

public void valueChanged(TreeSelectionEvent event) { 
  // don't do anything if disabled --- preventing cyclic updates! 
  if (!(disableEditorUpdate)) { 
    JTree tree = ((JTree) event.getSource()); 
    if (editorActivationListener.currentEditor != null && 
             tree.getLastSelectedPathComponent() instanceof SNodePointer) { 
      // grab the selected treee node 
      SNodePointer pointer = ((SNodePointer) tree.
      // disable the editor selection listener to prevent  
      // cyclic, never ending updates 
      // update the selection in the editor 

Swing's Artifacts: Tree Model and Renderer

of the implementation of the Swing artifacts.

Tree Cell Renderer

uses the getPresentation method on the nodes, and the IconManager to

public Component getTreeCellRendererComponent(JTree tree, Object object, 
             boolean selected, boolean expanded, 
             boolean leaf, int row, boolean hasFocus) { 
  if (object instanceof SNodePointer) { 
	string presentation; 
	DefaultTreeCellRenderer component; 
	read action { 
	  SNode node = ((SNodePointer) object).getNode(); 
	  presentation = node.getPresentation(); 
	  component = ((DefaultTreeCellRenderer) renderer.getTreeCellRendererComponent(
                     tree, presentation, selected, expanded, leaf, row, hasFocus)); 
  } else { 
    return renderer.getTreeCellRendererComponent(tree, object, selected, 
           expanded, leaf, row, hasFocus); 

Tree Model

node whose concept has been annotated with the
ShowConceptInOutlineAttribute attribute. It is stored in the

private List findAllChildrenWithAttribute(SNodePointer pointer) { 
  List result = new ArrayList(); 
  SNode node = pointer.getNode(); 
  if (node == null) { return new ArrayList(); } 
  foreach child in node.getChildren() { 
    SNode attribute = AttributeOperations.getNodeAttribute(
                          child.getConceptDeclarationNode(), "showInOutline"); 
    if (attribute != null) { 
  return result;