This document indicates the steps to follow in order to reproduce the tutorial.

The result of this tutorial is available in the folder part2-mmfirst-solution in githug repo or as a download in the following zip.

In the end you’ll obtain an editor that should look like this:

tutorial part2 result snowflake screenshot
Figure 1. Tutorial resulting editor on Snowflake model example
tutorial part2 result flower screenshot
Figure 2. Tutorial resulting editor on Flower model example

Similarly to programming where several program may produce the same behavior, it exists several ways to write Sirius diagram specification that draw the same diagram.

In this course, some attempt have been done to provide correct reuse of the rules and keep the global design simple. However, it can probably be improved. If you see improvement that do not blur the teaching goal feel free to contact the author and propose a change 😊.

1. Installation

Then

In case of trouble and error in your projects:

  • In the problem view, configure it in order to show only errors and warning on the selected project. This will help analyze the error (for example create missing empty folders or fix java minimum version)

  • you may have to regenerate the xtext editor code (right click on the Logo.xtext file → Run as → Generate Xtext Artifacts)

2. Sirius graphical editor

We will create a block based graphical representation for Logo.

Create the project for the graphical editor:

  • File → new → Viewpoint Specification Project

    • name: fr.inria.sed.logo.design

  • on the project (plugin.xml or manifest.mf) add a dependency to fr.inria.sed.logo.model project

  • open the odesign

    • rename the viewpoint from MyViewpoint to LogoBlockViewpoint

Let’s open some example using our representation. This can be done in the Modeling Workbench like xtext directly on .logo files.

Most parts of Sirius are interpreted, a big part of the diagram specification can be done directly in the Language Worbench and changes in the diagram specification are directly reported to the opened model. This greatly simplifies the design of the diagram editor.

However, since xtext does not work this way we need to convert our .logo files into .xmi that don’t require xtext.

You can directly create an xmi test file by :

  • open the ecore file → Select the LogoProgram class → right click → Create dynamic instance

This is the recommended way if you do not have an xtext representation.

In some situation you may wish to convert an xtext represensation into xmi. IE. convert a logo file into an xmi file. To do that:

  • in the Modeling workbench (with xtext support available…​)

  • right click on the .logo file → Open with → Other → Sample Reflective Ecore Model Editor

  • File → save as → choose a name ending with .xmi

  • verify that the file is correctly encoded in xmi (ie. a xml flavor) by opening it with the generic text editor

  • change the header in order to be

<logo:LogoProgram xmi:version="2.0" xmlns:xmi="http://www.omg.org/XMI"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:logo="http://www.inria.fr/sed/logo/Logo"
    xsi:schemaLocation="http://www.inria.fr/sed/logo/Logo ../fr.inria.sed.logo.model/model/Logo.ecore">

copy or import the file in some test project in the Language Workbench

For best result if working with both Sirius and Xtext. I recommend to open (import the project) the project containing the design file in both the Language workbench AND Modeling workbench.

Using this technique you can directly add a Sirius representation on top of the .logo files without converting them in xmi.

NOTE when using the second workbench, make sure to create the representation using the correct viewpoint, since it will appear twice.

For our logo example, we’ll mostly design the graphical representation from the modeling workbench.

In the odesign:

  • on the viewpoint; right click → new representation → diagram description

  • on the diagram description;

    • on the metamodel tab: add a reference to the ecore file (add from registry if you work with xtext and are working in the modeling workbench, otherwise use add from workspace)

    • on the general tab:

      • Domain class = LogoProgram (the completion should work)

      • give an ID = LogoBlockDiagram (change the label for "Logo Block Diagram")

      • tick "Initialization" and "show on startup"

2.1. Create a test model with its representation

Create a test project and copy one or several .logo files for testing the representation.

Right click on the .logo file → New → Representation file

This allows to create one file containing the representations (ie. the diagrams) for the given .logo file. These representations will be contained in an .aird file.

Sirius support another mode for the diagram using a project session:

When creating the project you can use the Modeling project wizard. Projects with this nature do not require to create manually the .aird file because it will create one by default for the project.

However in this case, all representations of all models in the current project will be contained in a single "representation.aird" file. While being convenient for some purposes, this behavior may not be suitable for all cases.

2.2. Display all root instructions:

  • New diagram element → Node then in the properties view

    • Id: PrimitiveInstructionNode

    • domain class: logo::PrimitiveInstruction (you can try with Instruction but you’ll probably have to change it later 😉 )

    • semantic candidate expression: aql:self.eContents() (alternative to reject some kinds : aql:self.eContents()->reject(x | x.oclIsKindOf(logo::ProcDeclaration))

    • New style → Dot

    • Label tab:

      • Label expression: aql:self.eClass().name (for a start, will be improved later)

      • Label position: border

    • Advanced tab:

      • allow resizing : uncheck

      • size computation expression: 1

you can try with a more generic type such as Instruction and then reject some elements using a query such as: aql:self.eContents()->reject(x | x.oclIsKindOf(logo::ProcDeclaration))

However, this will not fit our final design. and using the PrimitiveInstruction and ControlStructureInstruction structure of the metamodel allow to factorize some representation rules for each group.

If you have nice default icons defined in the .edit project, they’ll be directly displayed.

I recommend to use explicit names as IDs in Sirius. I usually start by the represented model element (ie. metaclass name) followed by the kind of representation (Container, Node, or edge) using camel case text.

2.3. Display all procedure declarations:

  • New diagram element → Container then in the properties view

    • Id: ProcedureDeclarationContainer

    • domain class: logo::ProcDeclaration (you can try with Instruction but you’ll probably have to change it later ;-) )

    • semantic candidate expression: aql:self.eContents() (no need for additional fiter here since we already said that it is limited to ProcDeclaration.

    • New style → Gradient

    • Label tab: Label expression: aql:self.name (for a start, will be improved later for example with aql: self.name + ' '+ self.args.xtextPrettyPrint() after Improve labels and xtext integration)

2.4. Display all instructions in the procedure declaration box:

We will indicates to the ProcedureDeclarationContainer container that we want to reuse some display rules.

  • On the procDeclNode,

    • Import tab, Reused Node Mapping: PrimitiveInstructionNode

2.5. Add a link representing the sequence of instructions

  • New diagram element → Relation based Edge then in the properties view

    • Id: instructionSequenceEdge

    • source mapping: PrimitiveInstructionNode

    • target mapping: PrimitiveInstructionNode

    • Target finder expression:

aql:let i = self.eContainer().instructions->asSequence() in i->at(i->indexOf(self)+1)

Explanation of the query above:

from self (a PrimitiveInstruction) retrieve its container (a Block or a LogoProgram) then get the list of instructions and store it in a variable named i. In this list of instruction compute the position of the Repeat block i->indexOf(self) Then returns the instruction that is at the following position.

As aql does not fails if the range is wrong, it will simply return null instead.

  • New diagram element → Relation based Edge then in the properties view

    • Id: procCallEdge

    • source mapping: PrimitiveInstructionNode

    • target mapping: procDeclNode

    • Target finder expression: aql:if self.oclIsKindOf(logo::ProcCall) then self.oclAsType(logo::ProcCall).declaration else null endif

    • make this link use dashed line

You can test your queries in order to write them: use the "Acceleo Model to Text > Interpreter" view then switch to "Sirius" mode instead of "Acceleo" mode.

Warning: When using the Interpreter view from an element selected in a Sirius representation, the context of the expression is not the semantic element, but the view model element used internally by Sirius.

In the interpreter view, to get the semantic element, you must enter aql:self.target

on the Logo Block Diagram

  • New diagram element → additional layer then in the properties view

    • Id: ProcedureCall

move procCallEdge to this layer

In the diagram, observe how to enable/disable the layer.

2.8. Add a default layout

On the Logo Block Diagram.

  • New layout → Composite layout then in the properties view

    • Padding: 20

    • top to bottom

Now hyou can use the Arrange all button to have a better looking diagram automatically.

2.9. Create representation for Repeat

On the default layer.

  • New diagram element → Container then in the properties view

    • Id: RepeatPartContainer

    • domain class: logo::Block

    • semantic candidate expression: aql: self.eContents()->filter(logo::Repeat)->collect(i | i.block))

    • New style → Gradient

      • Label tab:

        • Label expression: aql:'repeat ' +self.eContainer().condition.xtextPrettyPrint()

      • Color tab

        • Foreground color: light_blue

On the RepeatPartContainer

  • New diagram element → BorderedNode then in the properties view

    • Id: RepeatPartContainer

    • domain class: logo::Repeat

    • semantic candidate expression: aql:self.eContainer()

    • New style → BasicShape

      • General tab

        • Shape: square

      • Label tab:

        • Hide label by default

      • Advanced tab:

        • Authorized sides: North only

In order to reuse rules: in procDeclContainer

  • Import tab:

    • Reused Container Mapping: repeatPartContainer

Allow link from Primitive instruction to repeat:

In InstructionSequenceEdge

  • General tab:

    • Target Mapping: PrimitiveInstructionNode, Bordered RepeatBeginBNode

Create link from repeat to other instructions.

  • New diagram element → Relation based Edge then in the properties view

    • Id: endRepeatSequenceEdge

    • source mapping: RepeatPartContainer

    • target mapping: PrimitiveInstructionNode, Bordered RepeatBeginNode

    • Target finder expression:

aql:let i = self.eContainer().eContainer().instructions->asSequence() in i->at(i->indexOf(self.eContainer())+1)

Explanation of the query above which is a variant of the one in Add a link representing the sequence of instructions:

from self (a Block contained by a Repeat instruction) retrieve its container (a Repeat instruction) then its container (a Block or a LogoProgram) then get the list of instructions and store it in a variable named i. In this list of instruction compute the position of the Repeat block i->indexOf(self.eContainer()) Then returns the instruction (including the bordered RepeatBeginnode) that is at the following position.

2.10. Create representation for If

This section a quite similar to the one about the Repeat instruction and doesn’t introduce significant new Sirius concepts. (It simply creates a separate node for the If then links it to 2 blocks: thenPart and elsePart. Whereas Repeat creates a bordered node on a single Block.) If you do not have much time, you can jump to the next section Improve labels and xtext integration.

On the default layer.

  • New diagram element → Node then in the properties view

    • Id: IfNode

    • domain class: logo::If

    • semantic candidate expression: aql:self.eContents()

    • New style → Diamond

    • Label tab:

      • Label expression: aql:self.eClass().name (for a start, will be improved later)

      • Label position: border

    • Advanced tab:

      • allow resizing : uncheck

      • size computation expression: 3

  • New diagram element → Container then in the properties view

    • Id: thenPartContainer

    • domain class: logo::Block

    • semantic candidate expression: aql: self.eContents()->filter(logo::If)->collect(i | i.thenPart))

    • _New style → Gradient

    • Label tab:

      • Label expression: aql:'then'

    • Color tab

      • Foreground color: light_green

  • New diagram element → Container then in the properties view

    • Id: elsePartContainer

    • domain class: logo::Block

    • semantic candidate expression: aql: self.eContents()->filter(logo::If)->collect(i | i.elsePart))

    • _New style → gradient

    • Label tab:

      • Label expression: aql:'else'

    • Color tab

      • Foreground color: light_red

Do in all following containers: procDeclContainer, repeatPartContainer, thenPartContainer, and elsePartContainer;

  • Import tab:

    • Reused Node Mapping: PrimitiveIntrustionNode, IfNode

    • Reused Container Mapping: repeatPartContainer, elsePartContainer, thenPartContainer

On the Default layer.

  • New diagram element → Relation based Edge then in the properties view

    • Id: IfThenEdge

    • source mapping: IfNode

    • target mapping: thenPartContainer

    • semantic candidate expression: aql: self.thenPart

  • New diagram element → Relation based Edge then in the properties view

    • Id: IfElseEdge

    • source mapping: IfNode

    • target mapping: elsePartContainer

    • semantic candidate expression: aql: self.elsePart

  • New diagram element → Relation based Edge then in the properties view

    • Id: EndIfSequenceEdge

    • source mapping: thenPartContainer, elsePartContainer

    • target mapping: PrimitiveInstructionNode, IfNode

    • semantic candidate expression:

aql:let i = self.eContainer().eContainer().instructions->asSequence() in i->at(i->indexOf(self.eContainer())+1)
Exercise for the motivated: reproduce similar structure for Repeat and While control structure

3. Improve labels and xtext integration

We will create some java services to be used by sirius.

3.1. Add xtext aware service static methods

close the modeling worbench (will need to be restarted in order to take into account the new methods)

in the Language workbench.

in the xxx.design project open plugin.xml file, add a plugin dependency to org.eclipse.xtext, org.eclipse.ui.ide, org.eclipse.ui.workbench.texteditor, and org.eclipse.ui.workbench.

copy the file InfoPopUp.java in the package next to the Services.java class.

add the following methods in the Services.java file. (or copy the file from Services.java).

/**
 * Try to retrieve an xtext resource for the given element and then get its String representation
 * @param any EObject
 * @return the xtext representation of the EObject or an empty string
 */
public String xtextPrettyPrint(EObject any) {
    if (any != null && any.eResource() instanceof XtextResource && any.eResource().getURI() != null) {
        String fileURI = any.eResource().getURI().toPlatformString(true);
        IFile workspaceFile = ResourcesPlugin.getWorkspace().getRoot().getFile(new Path(fileURI));
        if (workspaceFile != null) {
            ICompositeNode node = NodeModelUtils.findActualNodeFor(any);
            if (node != null) {
                return node.getText().trim();
            }
        }
    }
    return "";
}

public EObject openTextEditor(EObject any) {
    if (any != null && any.eResource() instanceof XtextResource && any.eResource().getURI() != null) {

        String fileURI = any.eResource().getURI().toPlatformString(true);
        IFile workspaceFile = ResourcesPlugin.getWorkspace().getRoot().getFile(new Path(fileURI));
        if (workspaceFile != null) {
            IWorkbenchPage page = PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage();
            try {
                IEditorPart openEditor = IDE.openEditor(page, workspaceFile,
                        "fr.inria.sed.logo.xtext.Logo", true);
                if (openEditor instanceof AbstractTextEditor) {
                    ICompositeNode node = NodeModelUtils.findActualNodeFor(any);
                    if (node != null) {
                        int offset = node.getOffset();
                        int length = node.getTotalEndOffset() - offset;
                        ((AbstractTextEditor) openEditor).selectAndReveal(offset, length);
                    }
                }
                // editorInput.
            } catch (PartInitException e) {
                Activator.error(e.getMessage(), e);
            }
        }
    }
    System.out.println(any);
    return any;
}

public EObject openBasicHoveringDialog(EObject any) {
    String xtextString = xtextPrettyPrint(any);
    if (xtextString != null && !xtextString.isEmpty()) {
        IEditorPart part = PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage().getActiveEditor();
        InfoPopUp pop = new InfoPopUp( part.getSite().getShell() , "Textual representation of the element","press ESC to close");
        pop.setText(xtextString);
        pop.open();
    }
    return any;
}

3.2. Use services to improve labels

restart the modeling workbench

If you start the second eclipse in debug mode, small java changes (code in an existing method) can be taken into account without a full restart.

On the IfNode

  • Label tab

    • Label expression: aql:self.condition.xtextPrettyPrint()

On PrimitiveInstructionNode

  • New conditional style

    • Predicate expression: [self.oclIsKindOf(logo::Left) or self.oclIsKindOf(logo::Right) /]

    • copy the style of the PrimitiveInstructionNode into this new conditional style

      • Label tab

      • Label expression :

aql:self.eClass().name+' '+self.angle.xtextPrettyPrint()

do the same for other types such as Forward, Backward, ProcCall …​

service calling xtextPrettyPrint() might be usefull too in the tooltip expression on the General tab of the styles.

3.3. Add actions that open xtext editor

  • new tool → Section

    • Id: edition

3.3.1. Open xtext editor via right click popup

  • new menu → Popup menu

    • Id: OpenInTextEditorPopUp

    • Icon: add an icon from your own (or get one from the solution)

in the Begin element:

  • new operation → change context

    • browse expression: service:self.openTextEditor()

4. Some other actions and palette integration

4.1. Add action that create elements (Palette)

  • new element creation → node creation

    • Id: addPenUp (also change the label for a nicer name in the Paletter)

    • Node Mappings: PrimitiveInstructionNode

on Begin

  • new operation → change context

    • browse expression: var:container

      • new operation → create instance

        • reference name: instructions

        • Type name: logo::PenUp

4.2. Add Validation rule (error marker)

Sirius provide a way to define rules that’ll report errors. (Markers)

It is useful for example when creating element in sirius may lead to models that cannot be serialized in xtext.

The validation rule can also contains quickfix actions.