Creating an OPM GEF Editor – Part 23: Drag & Drop from the Palette

Previous tutorial: Creating an OPM GEF Editor – Part 22: Enabling Select-All Action in a GEF Editor

Doing D&D from the palette never seemed to me an important feature, but after seeing this question on SO, I thought to myself that this shouldn’t be so complicated. So I took the challenge, and after 1 hour of shoveling through the GEF examples, I found how this is done. Someday, someone must write good documentation for GEF… but in the meantime, this is how it is done.

First, you need to add two lines to the graphical editor’s configuration (lines between D&D comments):

protected void configureGraphicalViewer() {
  super.configureGraphicalViewer();
  getGraphicalViewer().setEditPartFactory(new OPMEditPartFactory());
  getActionRegistry().registerAction(new ToggleGridAction(getGraphicalViewer()));
  getActionRegistry().registerAction(new ToggleSnapToGeometryAction(getGraphicalViewer()));
  getGraphicalViewer().setContextMenu(
    new OPMGraphicalEditorContextMenuProvider(getGraphicalViewer(), getActionRegistry()));
  configureKeyboardShortcuts();

  // D&D
  getGraphicalViewer().addDropTargetListener(new TemplateTransferDropTargetListener(getGraphicalViewer()));
  getEditDomain().getPaletteViewer().addDragSourceListener(
    new TemplateTransferDragSourceListener(getEditDomain().getPaletteViewer()));
  // end D&D
}

I executed the new editor, and D&D still didn’t work… I was missing something. So I went and checked the Shapes
example and found that the palette entries created there are of type CombinedTemplateCreationEntry and
not the regular CreationToolEntry which I am using. This seemed in the right direction, so I changed one
of my palette entries:

private void addNodeTools() {
  CreationToolEntry entry =
    new CreationToolEntry("Label", "Create new Label", new LabelFactory(), ImageDescriptor.createFromFile(
      this.getClass(), "icons/label.ico"), ImageDescriptor.createFromFile(this.getClass(), "icons/label.ico"));
  group.add(entry);

  entry =
    new CombinedTemplateCreationEntry("OPMObject", "Create a new Object", new OPMObjectFactory(),
      ImageDescriptor.createFromFile(this.getClass(), "icons/object.ico"), ImageDescriptor.createFromFile(
        this.getClass(), "icons/object.ico"));
  entry.setToolClass(CreationAndDirectEditTool.class);
  group.add(entry);

I also had to alter the OPMObjectFactory so that the OPMObject so that it comes
with default constraints. Now fire up the editor… And there it goes! it works! it works!!!

Enjoy your D&D. As usual, you can browse the source code at github.

Next Turorial: Creating an OPM GEF Editor – Part 24: “Smart” Multi-line Text Figure

Enhanced by Zemanta

JJTV Presentation – Creating Graphical Editors with GEF

Last week I gave a short presentation in JJTV meetup on how to create graphical editors with Eclipse and GEF. This is a very short and fast tutorial that shows the basic concepts and how they are developed. The code that implements the examples is far from being complete, but for those who are just getting started with GEF, you may find this interesting.

The code and the presentation can be found here.

 

Enhanced by Zemanta

Welcome Korean Readers

This week I started receiving a relatively large number of hits from http://cafe.naver.com/eclipseplugin (relative to the number of hits I get every day). I couldn’t help and take a look at the source of the hits, which turned out to be a Korean eclipse developer forum, linking to some of my GEF tutorials.

So hello to all my new Korean visitors! come back any time :-)

Enhanced by Zemanta

Creating an OPM GEF Editor – Part 22: Enabling Select-All Action in a GEF Editor

Previous tutorial: Creating an OPM GEF Editor – Part 21: Adding Keyboard Shortcuts

Today I was working on a model using my OPM GEF editor, and wanted to select all elements of the model… but for some strange reason the “Select All” action in the “Edit” menu was disabled… So I started to investigate.

First, the “Select All” action should be similar to other eclipse standard actions, like “Undo” and “Delete”, so I expected some treatment similar to these actions – a SelectAllRetargetAction or something similar. There is a SelectAllAction class in the GEF framework, but I was not sure how to plug it in. First, I saw that this action was already registered in the actionRegistry in GraphicalEditor.createActions() function. So why was it disabled? More code reading showed me that also the undo and redo actions are registered there, but I also had to create for them a retargetable action in the ActionBarContributor of my editor. So I added this code to the OPMGraphicalEditorActionBarContributor.buildActions() method (the last line):

	@Override
	protected void buildActions() {
		addRetargetAction(new UndoRetargetAction());
		addRetargetAction(new RedoRetargetAction());
		addRetargetAction(new DeleteRetargetAction());
		addRetargetAction(new RetargetAction(GEFActionConstants.TOGGLE_GRID_VISIBILITY,
				GEFMessages.ToggleGrid_Label,
				IAction.AS_CHECK_BOX));
		addRetargetAction(new RetargetAction(GEFActionConstants.TOGGLE_SNAP_TO_GEOMETRY,
				GEFMessages.ToggleSnapToGeometry_Label,
				IAction.AS_CHECK_BOX));
		addRetargetAction(new RetargetAction(GEFActionConstants.SELECT_ALL, GEFMessages.SelectAllAction_Label));
	}

The “Select All” menu item became available and the action worked, even with the expected “Ctrl+A” keyboard accelerator. But the editor showed me that the GEFActionConstants.SELECT_ALL constant is deprecated, and that I should use the ActionFactory.SELECT_ALL.getId()… I scratched my head a bit… I had a new lead on my problem, but where to search for clues? Obviously, the org.eclipse.gef.examples.logic project! And there it was: the ActionBarContributor.declareGlobalActionKeys() function calls addGlobalActionKey(ActionFactory.SELECT_ALL.getId()). In two minutes I removed my old line and added this one to my contributor class, and there it was again, the “Select All” action worked, and the code was clean of warnings.

Seems strange to me that although the action was already registered, only when I added here the action became available. The name of the function (and it’s location) is also confusing: why the Bar in ActionBarContributor? And another nice thing in the code of this class is the warning in the class comment:

/**
 * Contributes actions to the workbench. !!Warning: This class is subject to
 * change.
 */

But the “Select All” action works, and that is what matters.

Next tutorial: Creating an OPM GEF Editor – Part 23: Drag & Drop from the Palette

GEF Internals Part 2 – Mouse Interaction and the Creation Tool

After investigating the complex interactions that occur with the a title=”GEF Internals Part 1 – Mouse Interaction and the Selection Tool” href=”http://www.vainolo.com/2012/01/01/gef-internals-part-1-mouse-interaction-and-the-selection-tool/”selection tool in my previous post/a, we will now focus on a simpler case, the creation tool.

When the mouse moves over the canvas, the codeLightweightSystem/code system catches the mouse move event and forwards it to the codeDomainEventDispatched/code. If the event was not previously captured by the domain codeDomainEventDispatcher/code executes direct codedraw2d/code interaction. I am not completely sure what “captured by the domain” means, but for example when there is a mouse click, which is translated into a codemouseDown/code event, and a codemouseUp/code event, if the codemouseDown/code event was handled by the domain, then then codemouseUp/code event is “captured by the domain” and no direct codedraw2d/code interaction is allowed. Anyway, after codedraw2d/code interaction is executed (or is not executed), the codeDomainEventDispatcher/code can forward the event to the domain as either a codemouseMove/code or as a codemouseDrag/code event (depending of the states of the buttons). This can be seen in the following sequence diagram:

a href=”http://www.vainolo.com/wp-content/uploads/2012/01/dispatchMouseMoved.png”img class=”aligncenter size-full wp-image-959″ title=”dispatchMouseMoved” src=”http://www.vainolo.com/wp-content/uploads/2012/01/dispatchMouseMoved.png” alt=”" width=”422″ height=”497″ //a

When the domains receives the codemouseMove/code, it searches for the currently active tool and forwards it the codemouseMove/code request. This is the place where finally some work is done. The domain calls codeTool.mouseMove/code which is implemented in the codeAbstractTool/code class. This method does some internal work that I could not understand but seems harmless, and after this it calls codehandleMove/code, an internal method that tools should override to implement their functionality. The codeCreationTool.handleMove/code method works as follows: First it creates of updates a codeCreateRequest/code instance that contains the information used by the codeCreationTool/code, which includes the mouse location, factory that creates the new domain instances, and other things. Since the mouse can be moved a lot over the canvas, it is not logical to create a new request every time a mouse move is captured, so the codeCreationTool/code has a cached request which it updates every time a codemouseMove/code event is accepted.

After the codeCreateRequest/code is created or updated, the codeCreationTool/code updates the target of the request… This happens in two steps: First the tool finds the topmost codeEditPart/code below the current mouse location that can handle a codeCreateRequest/code. Second, it requests from the codeEditPart/code for the emtarget/em of the request… what? didn’t we just fetch the target of the request from the diagram? Well, it seems that GEF allows the codeEditPart/code under the mouse to give us another codeEditPart/code as the real target of the request. Why? I can think of a number of reasons, for example we can have an editor where you have squares and a button on the inner top left corner of the square and every time you click this button a new circle is added inside the square. So the codeEditPart/code that receives the codehandleMove/code is the button, but the codeEditPart/code that handles the request is the enclosing square, probably the parent of the button.

Now that GEF has the real codeEditPart/code that will handle the request, and the codeCreateRequest/code itself, it asks the target codeEditPart/code to create the codeCommand/code that will be executed if the tool is applied (mouse click). This command is created in the codeEditPart/code by running over all the codeEditPolicy/code instances that have been installed in the codeEditPart/code and asking them to create a codeCommand/code for the provided request. If more than one codeEditPart/code returns a codecommand/code, they are are chained inside a codeCompoundCommand/code. After the command is created, the codeTool/code refreshes the cursor that is displayed on the editor – and this is where we get the functionality that changes the cursor to an X when the current codeTool/code cannot be applied to the target under the mouse, because the command that was return by the codeEditPart/code cannot be executed.

Finally, the tool requests feedback from the target codeEditPart/code. Note that here the target may not be exactly the codeEditPart/code below the cursor. This feedback is requested in the same way as the codeCommand/code, by delegating it to the codeEditPolicy/code instances that are installed in the codeEditPart/code.

I have captured this interaction in the following sequence diagram:
a href=”http://www.vainolo.com/wp-content/uploads/2012/01/mouseMoveHandling.png”img src=”http://www.vainolo.com/wp-content/uploads/2012/01/mouseMoveHandling.png” alt=”" title=”mouseMoveHandling” width=”879″ height=”723″ class=”aligncenter size-full wp-image-961″ //a

Wasn’t that interesting? Now, you can see that having complex edit policies can be very hazardous for your editor, since they may be called on every mouse move! Furthermore, all the classes perform a LOT of functionality, and definitely do not conform to the a href=”http://en.wikipedia.org/wiki/SOLID_(object-oriented_design)”SOLID/a principles, specially a href=”http://en.wikipedia.org/wiki/Single_responsibility_principle”Single responsibility/a, which makes the code harder to understand. But I have to say that the architecture is beautiful. I’m still looking into GEF all the time, and will post my findings as I go, but if you have a special topic that you’d like to have answered, leave me a commend and I’ll see what I can do.

GEF Internals Part 1 – Mouse Interaction and the Selection Tool

There are some explanations on the net on how GEF works (see here ) but I have not found a good description on how GEF really works, so I will try to unravel the secret and post it here for the world to know. My investigation began when I found that although GEF EditPolicy instances are installed in the EditPart using a key, this key was never used (well, everything works even if I changed all the keys to “chukumuku”). I started reading more and mode code and was fascinated about how the framework works. So here it goes, what I have found in some nights of code reading and debugging.

We will start by explaining what happens when we select a tool and move the mouse over the editor. The editor itself is implemented on top of SWT (The Standard Widget Toolkit), which is like java’s AWT (Abstract Window Toolkit). My knowledge of SWT is pretty low so it won’t be explained here. The only important thing to know is that a GEF editor’s graphics are drawn on an SWT Canvas on which GEF draws the RootFigure which is used as the top level figure on which the editor is drawn. The Canvas is par of a LightweightSystem class who forwards user interaction from SWT to GEF. There is also all the drawing functionality of draw2d that is transferred in the other direction but this will not be discussed here.

Looking at this from the other side, when a GEF editor is started, the platform calls createPartControl on the GraphicalEditorClass. This function in turn calls createGraphicalViewer which instantiates a new LightweigthSystem and a Canvas on which to draw the EditPart instances. The process is better described in the following sequence diagram:

So knowing this we now go into what happens when the user interacts with the editor, using the SelectionTool for our example. As explained above, the LightweightSystem bridges the requests coming from the SWT Canvas, so when the user clicks on the editor canvas, the request that is fired by it is received by the LightweightSystem‘s EventHandler. The EventHandler is an adapter to a EventDispatcher which does the real event handling. In our case this is a DomainEventDispatcher which has a handle to the EditDomain containing our editor’s information.
Event handling is done in two parts: direct interaction and indirect interaction (my naming).

  1. Direct Interaction: while we are using GEF for editor interaction, draw2d also allows us to add mouse listeners that can interact with the figures in the diagram. In this part, the dispatcher searched for draw2d listeners in the figures below the mouse click, and if it finds one it calls the listener with the mouse event. If there is more than one listener in the Figure hierarchy, the topmost listener (z-axis) is called.
  2. Indirect Interaction: the editor directly handles the mouse event by forwarding the event to the currently active tool, in this case the SelectionTool. The selection tool is actually a complex example since it does not work directly but delegates to a DragEditPartsTracker which does the selection. This is done because we can drag the selection tool to do multiple selection. In any case, the selection tool forwards the request to the DragEditPartsTracker which finds what EditPart instances are now selected in the editor, updates the domain’s internal data structures and fires a selection changed event, which updates the graphics on the screen to match the requested selection.

I tried to catch this interaction in the following two sequence diagrams: First the generic part before the SelectionTool is invoked,

And second, the selection tool is invoked with the mouse event.

I hope that this explanation has shed a bit of light on how GEF works. For me it has been a very hard task understanding what goes on under the hood, and there are still many things that need to be cleared out. I think that in the next post I’ll show how the creation tool works, which I think is simpler and in retrospect I should have analyzed first. So see you next time.

Creating an OPM GEF Editor – Part 15: Adding Structural Links

Previous Tutorial: Creating an OPM GEF Editor – Part 14: Refactoring, Refactoring and More Refactoring.

In the last tutorial we learned how to refactor classes using the build-in operations in eclipse. While automatic refactoring is very useful, there are times when you just have to do the changes manually. For example, the OPMThingEditPart creates a new OPMNodeAdapter in its constructor. But this should be done in the constructor of the OPMNodeEditPart. Sadly this change can only be done manually.
Furthermore, we now have to divide the createEditPolicies function in two: one part is generic for the OPMNodeEditPart and the other part is more specific, only for OPMThingEditPart (direct editing). So to start this tutorial I did some broad changes in the code base so that the generic functionality of the OPMNodeEditPart could also be used for structural links. So before you start this tutorial, please download the latest project files can be found here: GEF.Editor.Tutorial.15.start.zip. This tutorial is going to be specially long so grab a bag of doritos and some salsa just in case you get hungry :-)

Structural links in OPM are displayed using a symbol that aggregates all of the links that start at a OPMThing, so if there is more than one link that starts at this OPMThing, the links are displayed as one link that starts at the OPMThing and goes to an aggregator, and from this aggregator there is one link to every target OPMThing as can be seen here. The way I decided to implement this is for the user to use a create connection tool to add a new structural link, but when the tool is applied (that is, after source and destination nodes have been selected) instead of creating a link, the activated policy does extra work: if there is no matching structural link that starts at the source thing, it creates a new aggregator and connects it between the source and the target. But if there is already an aggregator that starts at the source thing, it only adds a new link from the aggregator to the target link. Some other things must also be handled, like that removal of nodes now requires also removal of structural links that start at the deleted node. Unlike previous tutorials, the amount of code in this tutorial is pretty large, therefore only highlights will be shown here, but as usual the full code is available at the end of the tutorial.

  1. We’ll start with the simple things. To create a structural link we add new factories and tools to the editor.
    The factory creates a new OPMStructuralLinkAggregator instances and sets its kind:

    package com.vainolo.phd.opm.gef.editor.factory;
    
    import org.eclipse.gef.requests.CreationFactory;
    
    import com.vainolo.phd.opm.model.OPMFactory;
    import com.vainolo.phd.opm.model.OPMStructuralLinkAggregator;
    import com.vainolo.phd.opm.model.OPMStructuralLinkAggregatorKind;
    
    /**
     * Factory used by palette tools to create {@link OPMStructuralLinkAggregator} of
     * {@link OPMStructuralLinkAggregatorKind#AGGREGATION} kind. 
     * @author vainolo
     *
     */
    public class OPMAggregationStructuralLinkAggregatorFactory implements CreationFactory {
    
        @Override public Object getNewObject() {
            OPMStructuralLinkAggregator aggregator = OPMFactory.eINSTANCE.createOPMStructuralLinkAggregator();
            aggregator.setKind(OPMStructuralLinkAggregatorKind.AGGREGATION);
            return aggregator;
        }
    
        @Override public Object getObjectType() {
            return OPMStructuralLinkAggregator.class;
        }
    }
    

    And we add ConnectionCreationToolEntry entries to the OPMGraphicalEditorPalette:

        /**
         * Add tools to create structural links in the diagram.
         */
        private void addOPMStructuralLinkTools() {
            ConnectionCreationToolEntry entry;
            entry = new ConnectionCreationToolEntry("Aggregation", "Create a new Aggregation link", new OPMAggregationStructuralLinkAggregatorFactory(), null, null);
            group.add(entry);
            entry = new ConnectionCreationToolEntry("Exhibition", "Create a new Exhibition link", new OPMExhibitionStructuralLinkAggregatorFactory(), null, null);
            group.add(entry);
            entry = new ConnectionCreationToolEntry("Generalization", "Create a new Generalization link", new OPMGeneralizationStructuralLinkAggregatorFactory(), null, null);
            group.add(entry);
        }
    
  2. The connection creation tools used to add the structural link activate the OPMNodeGraphicalNodeEditPolicy that is installed in the source and target OPMNode. A connection between two diagram nodes is created in two steps, first when the user clicks on the source node (calling the getConnectionCreateCommand) and second when the user clicks on the target node (calling the getConnectionCompleteCommand). These functions are called by the framework before the user actually clicks on the source node to check if the node can be set as a source of the link (changing the icon that is shown on the screen), therefore in the policies we also limit the creation of links by returning null when the current location is not a valid source/target for the link.
    All of the functionality for creating the structural link is done in the getConnectionCompleteCommand as follows:

    • We check the outgoing structural links of the OPMNode (I implemented a new function to the model to provide this functionality) for a link that ends at an OPMStructuralLinkAggregator of the kind that was requested by the tool.
    • If we find such a link, our only job is to add a new link from the aggregator to the target OPMNode.
    • If we didn’t find such a link, we must add a new aggregator to the diagram and connect it to the source and target nodes. The aggregator’s location is set between the source and target nodes.

    Note that because of the refactoring we did before this tutorial, we can use the existing OPMLinkCreateCommand and OPMNodeCreateCommand and aggregate them using CompoundCommand instances.

    package com.vainolo.phd.opm.gef.editor.policy;
    
    import org.eclipse.draw2d.geometry.Dimension;
    import org.eclipse.draw2d.geometry.Point;
    import org.eclipse.draw2d.geometry.Rectangle;
    import org.eclipse.gef.commands.Command;
    import org.eclipse.gef.commands.CompoundCommand;
    import org.eclipse.gef.commands.UnexecutableCommand;
    import org.eclipse.gef.editpolicies.GraphicalNodeEditPolicy;
    import org.eclipse.gef.requests.CreateConnectionRequest;
    import org.eclipse.gef.requests.ReconnectRequest;
    
    import com.vainolo.phd.opm.gef.editor.command.OPMLinkCreateCommand;
    import com.vainolo.phd.opm.gef.editor.command.OPMNodeCreateCommand;
    import com.vainolo.phd.opm.gef.editor.part.OPMStructuralLinkAggregatorEditPart;
    import com.vainolo.phd.opm.model.OPMFactory;
    import com.vainolo.phd.opm.model.OPMLink;
    import com.vainolo.phd.opm.model.OPMNode;
    import com.vainolo.phd.opm.model.OPMObjectProcessDiagram;
    import com.vainolo.phd.opm.model.OPMStructuralLinkAggregator;
    import com.vainolo.phd.opm.model.OPMThing;
    
    /**
     * Policy used to connect two nodes in the diagram. Currently connections can
     * only be created between two {@link OPMThing} instances.
     * 
     * @author vainolo
     */
    public class OPMNodeGraphicalNodeEditPolicy extends GraphicalNodeEditPolicy {
    
        private static final Dimension DEFAULT_AGGREGATOR_DIMENSION = new Dimension(30, 30);
    
        /**
         * Create a command used to begin connecting to nodes.
         * {@link OPMStructuralLinkAggregatorEditPart} nodes cannot be source nodes,
         * therefore in this case a {@link UnexecutableCommand} is returned.
         * 
         * @return a {@link Command} that contains the initial information neede to
         *         create a connection between two nodes.
         */
        @Override
        protected Command getConnectionCreateCommand(CreateConnectionRequest request) {
            // We must return null and not the usual UnexecutableCommand because is we return a
            // non-null value the framework thinks that the link can be created from this host,
            // something that we don't want to happen.
            if(request.getSourceEditPart() instanceof OPMStructuralLinkAggregatorEditPart) {
                return null;
            }
    
            if(request.getNewObject() instanceof OPMStructuralLinkAggregator) {
                request.setStartCommand(new Command() {});
                return request.getStartCommand();
            }
    
            OPMLinkCreateCommand result = new OPMLinkCreateCommand();
            result.setSource((OPMNode) getHost().getModel());
            result.setLink((OPMLink) request.getNewObject());
            result.setOPD(((OPMNode) getHost().getModel()).getOpd());
            request.setStartCommand(result);
            return result;
        }
    
        /**
         * Retrieves the command created by
         * {@link OPMNodeGraphicalNodeEditPolicy#getConnectionCreateCommand(CreateConnectionRequest)
         * getConnectionCreateCommand}, and adds it information so that the command
         * can be executed. {@link OPMStructuralLinkAggregatorEditPart} nodes cannot
         * be source nodes, therefore in this case a {@link UnexecutableCommand} is
         * returned.
         * 
         * @return a {@link Command} that can be executed to create a connection
         *         between two nodes.
         */
        @Override
        protected Command getConnectionCompleteCommand(CreateConnectionRequest request) {
            // A null command must be returned (and not the usual UnexecutableCommand),
            // otherwise GEF shows the used a symbol that the connection can be completed but
            // when the used clicks it is not created.
            if(request.getStartCommand() == null || 
               request.getTargetEditPart() instanceof OPMStructuralLinkAggregatorEditPart) {
                return null;
            }
    
            Command command = null;
    
            if(request.getNewObject() instanceof OPMStructuralLinkAggregator) {
                // create a structural link based on the request.
                command = handleOPMStructuralLinkRequest(request);
            } else {
                // Simple link creation command.
                OPMLinkCreateCommand linkCreateCommand = (OPMLinkCreateCommand) request.getStartCommand();
                linkCreateCommand.setTarget((OPMNode) getHost().getModel());
                command = linkCreateCommand;
            }
    
            return command;
        }
    
        /**
         * When the user requests the creation of a structural link, the following is done:
         * <ol>
         *   <li>If this is the first structural link of its kind between the source
         *       and target nodes, we create a new aggregator and connect it to the source
         *       and target.</li>
         *   <li>If there already is an aggregator of its kind between the nodes, we
         *       only add a new link from the aggregator to the new target.</li>
         * </ol>
         * 
         * @param request
         *            the user request to create a new strucutral link between the
         *            nodes.
         * @return a command that creates the links as stated above.
         */
        private Command handleOPMStructuralLinkRequest(CreateConnectionRequest request) {
            Command command = null;
    
            OPMNode sourceNode = (OPMNode) request.getSourceEditPart().getModel();
            OPMNode targetNode = (OPMNode) request.getTargetEditPart().getModel();
            OPMStructuralLinkAggregator aggregatorNode = (OPMStructuralLinkAggregator) request.getNewObject();
    
            boolean aggregatorFound = false;
            for(OPMLink structuralLink : sourceNode.getOutgoingStructuralLinks()) {
                OPMStructuralLinkAggregator existingAggregator = (OPMStructuralLinkAggregator) structuralLink.getTarget();
                if (existingAggregator.getKind() == aggregatorNode.getKind()) {
                    aggregatorFound = true;
                    aggregatorNode = existingAggregator;
                }
            }
    
            if (aggregatorFound) {
                // Just create a link from the aggregator to the target.
                OPMLinkCreateCommand linkCreateCommand = new OPMLinkCreateCommand();
                linkCreateCommand.setSource(aggregatorNode);
                linkCreateCommand.setTarget(targetNode);
                linkCreateCommand.setOPD(aggregatorNode.getOpd());
                linkCreateCommand.setLink(OPMFactory.eINSTANCE.createOPMLink());
    
                command = linkCreateCommand;
            } else {
                // Create a compound command consisting of three commands.
                CompoundCommand compoundCommand = new CompoundCommand();
                // create aggregator
                OPMNodeCreateCommand createAggregatorCommand = createCreateAggregatorNodeCommand(sourceNode, targetNode, aggregatorNode);
                compoundCommand.add(createAggregatorCommand);
                // connect aggregator to source.
                OPMLinkCreateCommand sourceLinkCreateCommand = createCreateOPMLlinkCreateCommand(sourceNode, aggregatorNode, sourceNode.getOpd());
                compoundCommand.add(sourceLinkCreateCommand);
                // connect aggregator to target.
                OPMLinkCreateCommand targetLinkCreateCommand = createCreateOPMLlinkCreateCommand(aggregatorNode, targetNode, sourceNode.getOpd());
                compoundCommand.add(targetLinkCreateCommand);
    
                command = compoundCommand;
            }
    
            return command;
        }
    
        /**
         * Helper function to create a command that connects two nodes with a
         * factory generated link.
         * 
         * @param source
         *            the source of the link.
         * @param target
         *            the target of the link.
         * @return
         */
        private OPMLinkCreateCommand createCreateOPMLlinkCreateCommand(OPMNode source, OPMNode target, OPMObjectProcessDiagram opd) {
            OPMLinkCreateCommand command = new OPMLinkCreateCommand();
            command.setSource(source);
            command.setTarget(target);
            command.setOPD(opd);
            command.setLink(OPMFactory.eINSTANCE.createOPMLink());
            return command;
        }
    
        /**
         * Create a command that adds the provided
         * {@link OPMStructuralLinkAggregator} to the diagram located between the
         * source and the target {@link OPMNode}.
         * 
         * @param source
         *            the source for the structural link.
         * @param target
         *            the target of the structural link.
         * @param aggregator
         *            the aggregator that should be added to the diagram.
         * @return A {@link OPMNodeCreateCommand} whose execution add the aggregator
         *         to the diagram.
         */
        public OPMNodeCreateCommand createCreateAggregatorNodeCommand(OPMNode source, OPMNode target, OPMNode aggregator) {
            OPMNodeCreateCommand command = new OPMNodeCreateCommand();
            command.setNode(aggregator);
            command.setParent(source.getOpd());
    
            // Calculate location of aggregator, between the source and targetnodes.
            Rectangle sourceConstraints = source.getConstraints();
            Rectangle targetConstraints = target.getConstraints();
            Point sourceCenter = new Point(sourceConstraints.x + sourceConstraints.width/2, sourceConstraints.y + sourceConstraints.height/2);
            Point targetCenter = new Point(targetConstraints.x + targetConstraints.width/2, targetConstraints.y + targetConstraints.height/2);
            Point aggregatorLeftTopCorner = new Point();
            aggregatorLeftTopCorner.x = sourceCenter.x + (targetCenter.x - sourceCenter.x)/2 - DEFAULT_AGGREGATOR_DIMENSION.width/2;
            aggregatorLeftTopCorner.y = sourceCenter.y + (targetCenter.y - sourceCenter.y)/2 - DEFAULT_AGGREGATOR_DIMENSION.height/2;
            if (aggregatorLeftTopCorner.x < 0) {
                aggregatorLeftTopCorner.x = 0;
            }
            if (aggregatorLeftTopCorner.y < 0) {
                aggregatorLeftTopCorner.y = 0;
            }
            command.setConstraints(new Rectangle(aggregatorLeftTopCorner, DEFAULT_AGGREGATOR_DIMENSION));
    
            return command;
        }
    
        @Override
        protected Command getReconnectTargetCommand(ReconnectRequest request) {
            return null;
        }
    
        @Override
        protected Command getReconnectSourceCommand(ReconnectRequest request) {
            return null;
        }
    }
    
  3. Since we are already working with policies, we might as well work on the OPMNodeComponentEditPolicy which is activated when the user asks to delete a OPMNode. What we do here is extend the removal functionality to handle cases when the node is the source of a structural aggregator, which means that the aggregator must be deleted; and when the node is the target of an aggregator who's only target is the deleted node, again needing removal of the aggregator. Note that since aggregators are OPMNode instances, we can also delete it like regular nodes, deleting all their source and target connections.
    package com.vainolo.phd.opm.gef.editor.policy;
    
    import org.eclipse.gef.EditPolicy;
    import org.eclipse.gef.commands.Command;
    import org.eclipse.gef.commands.CompoundCommand;
    import org.eclipse.gef.editpolicies.ComponentEditPolicy;
    import org.eclipse.gef.requests.GroupRequest;
    
    import com.vainolo.phd.opm.gef.editor.command.OPMNodeDeleteCommand;
    import com.vainolo.phd.opm.model.OPMLink;
    import com.vainolo.phd.opm.model.OPMNode;
    import com.vainolo.phd.opm.model.OPMStructuralLinkAggregator;
    import com.vainolo.phd.opm.model.OPMThing;
    
    /**
     * {@link EditPolicy} used for delete requests.
     * 
     * @author vainolo
     * 
     */
    public class OPMNodeComponentEditPolicy extends ComponentEditPolicy {
    
        /**
         * Create a command to delete a node. When a node is deleted all incoming
         * and outgoing links are also deleted (functionality provided by the
         * command). When a {@link OPMThing} node is deleted, there is special
         * treatment for structural links that start and end at this node. If this
         * node is source for a structural link, the
         * {@link OPMStructuralLinkAggregator} of this link must be deleted. Also if
         * this node is the target of the only outgoing link of a
         * {@link OPMStructuralLinkAggregator}, the aggregator must be deleted.
         * 
         * @return a command that deletes a node and all other required diagram
         *         entities.
         */
        @Override
        protected Command createDeleteCommand(GroupRequest deleteRequest) {
            OPMNode nodeToDelete = (OPMNode) getHost().getModel();
            CompoundCommand compoundCommand = new CompoundCommand();
    
            // For every outgoing structural link, create a command to delete the aggregator
            // node at the end of the link.
            for(OPMLink outgoingStructuralLink : nodeToDelete.getOutgoingStructuralLinks()) {
                OPMNode aggregatorNode = outgoingStructuralLink.getTarget();
                OPMNodeDeleteCommand aggregatorNodeDeleteCommand = new OPMNodeDeleteCommand();
                aggregatorNodeDeleteCommand.setNode(aggregatorNode);
                compoundCommand.add(aggregatorNodeDeleteCommand);
            }
            // For every incoming structural link whose aggregator has only one outgoing
            // link, create a command to delete the aggregator.
            for(OPMLink incomingStructuralLink : nodeToDelete.getIncomingStructuralLinks()) {
                OPMNode aggregatorNode = incomingStructuralLink.getSource();
                if(aggregatorNode.getOutgoingLinks().size() == 1) {
                    OPMNodeDeleteCommand aggregatorNodeDeleteCommand = new OPMNodeDeleteCommand();
                    aggregatorNodeDeleteCommand.setNode(aggregatorNode);
                    compoundCommand.add(aggregatorNodeDeleteCommand);
                }
            }
    
            // Create a command to delete the node.
            OPMNodeDeleteCommand nodeDeleteCommand = new OPMNodeDeleteCommand();
            nodeDeleteCommand.setNode((OPMNode) getHost().getModel());
            compoundCommand.add(nodeDeleteCommand);
    
            return compoundCommand;
        }
    }
    
  4. Now we have to implement all of the regular GEF functionality for the new EditPart. The OPMStructuralLinkAggregator is represented in the diagram as a triangle figure that I had to implement similarly to the existing triangle implementation provided by org.eclipse.draw2d.Triangle.
    
    package com.vainolo.phd.opm.gef.editor.figure;
    
    import org.eclipse.draw2d.Graphics;
    import org.eclipse.draw2d.Orientable;
    import org.eclipse.draw2d.PositionConstants;
    import org.eclipse.draw2d.Shape;
    import org.eclipse.draw2d.geometry.Point;
    import org.eclipse.draw2d.geometry.PointList;
    import org.eclipse.draw2d.geometry.Rectangle;
    
    /**
     * A triangle that uses all of its bounds to draw an isosceles triangle in the
     * figure's bounds, like this:
     * 
     * <pre>
     *      ______
     *     |  /\  |
     *     | /  \ | (bounds shown as surrounding rectangle). 
     *     |/____\|
     * </pre>
     * 
     * The implementation is based on the {@link org.eclipse.draw2d.Triangle}
     * implementation.
     * 
     * @author vainolo
     * 
     */
    public final class IsoscelesTriangle extends Shape implements Orientable {
        /** The points of the triangle. */
        protected PointList triangle = new PointList(3);
        /**
         * The direction this triangle will face. Possible values are
         * {@link PositionConstants#NORTH}, {@link PositionConstants#SOUTH},
         * {@link PositionConstants#EAST} and {@link PositionConstants#WEST}.
         */
        protected int direction = NORTH;
    
        /**
         * {@inheritDoc}
         */
        public void primTranslate(int dx, int dy) {
            super.primTranslate(dx, dy);
            triangle.translate(dx, dy);
        }
    
        /**
         * {@inheritDoc}
         */
        @Override
        protected void outlineShape(Graphics graphics) {
            graphics.drawPolygon(triangle);
        }
    
        /**
         * {@inheritDoc}
         */
        @Override
        protected void fillShape(Graphics graphics) {
            graphics.fillPolygon(triangle);
        }
    
        /**
         * Validates the figure, drawing a vertical isosceles triangle filling the
         * figure's bounds.
         */
        @Override
        public void validate() {
            super.validate();
            Rectangle r = getBounds().getCopy();
            r.shrink(getInsets());
            r.resize(-1, -1);
            Point p1 = null, p2 = null, p3 = null;
            switch(direction) {
            case NORTH:
                p1 = new Point(r.x + r.width / 2, r.y);
                p2 = new Point(r.x, r.y + r.height);
                p3 = new Point(r.x + r.width, r.y + r.height);
                break;
            case SOUTH:
                p1 = new Point(r.x + r.width / 2, r.y + r.height);
                p2 = new Point(r.x, r.y);
                p3 = new Point(r.x + r.width, r.y);
                break;
            case EAST:
                p1 = new Point(r.x, r.y);
                p2 = new Point(r.x + r.width, r.y + r.height / 2);
                p3 = new Point(r.x, r.y + r.height);
                break;
            case WEST:
                p1 = new Point(r.x + r.width, r.y);
                p2 = new Point(r.x + r.width, r.y + r.height);
                p3 = new Point(r.x, r.y + r.height / 2);
            }
            triangle.removeAllPoints();
            triangle.addPoint(p1);
            triangle.addPoint(p2);
            triangle.addPoint(p3);
        }
    
        /**
         * This functions is ignored. Use
         * {@link IsoscelesTriangle#setDirection(int)} instead.
         */
        @Override
        @Deprecated
        public void setOrientation(final int orientation) {
        }
    
        /**
         * {@inheritDoc}
         */
        @Override
        public void setDirection(final int direction) {
            if(direction != NORTH && direction != SOUTH && direction != EAST
                    && direction != WEST) {
                throw new IllegalArgumentException("Invalid direction " + direction);
            }
            this.direction = direction;
            revalidate();
            repaint();
        }
    }
    

    Having the figure, we have to create an EditPart that will be displayed by the GEF framework when it finds a OPMStructuralLinkAggregator instance. The code is pretty staightforward:

    
    package com.vainolo.phd.opm.gef.editor.part;
    
    import org.eclipse.draw2d.IFigure;
    import org.eclipse.gef.GraphicalEditPart;
    
    import com.vainolo.phd.opm.gef.editor.figure.OPMStructuralLinkAggregatorFigure;
    import com.vainolo.phd.opm.model.OPMStructuralLinkAggregator;
    
    public class OPMStructuralLinkAggregatorEditPart extends OPMNodeEditPart {
    
        private IFigure figure;
    
        @Override
        protected IFigure createFigure() {
            OPMStructuralLinkAggregator model = (OPMStructuralLinkAggregator) getModel();
            figure = new OPMStructuralLinkAggregatorFigure(model.getKind());
            return figure;
        }
    
        @Override
        protected void refreshVisuals() {
            OPMStructuralLinkAggregator model = (OPMStructuralLinkAggregator) getModel();
            ((GraphicalEditPart) getParent()).setLayoutConstraint(this, figure,model.getConstraints());
        }
    }
    

    And of course, we have to add this to the OPMEditPartFactory:

    package com.vainolo.phd.opm.gef.editor.part;
    
    import com.vainolo.phd.opm.model.OPMLink;
    import com.vainolo.phd.opm.model.OPMObject;
    import com.vainolo.phd.opm.model.OPMObjectProcessDiagram;
    import com.vainolo.phd.opm.model.OPMProceduralLink;
    import com.vainolo.phd.opm.model.OPMProcess;
    import com.vainolo.phd.opm.model.OPMStructuralLinkAggregator;
    
    import org.eclipse.gef.EditPart;
    import org.eclipse.gef.EditPartFactory;
    
    public class OPMEditPartFactory implements EditPartFactory {
    
    	@Override
    	public EditPart createEditPart(EditPart context, Object model) {
    		EditPart part = null;
    
    		if (model instanceof OPMObjectProcessDiagram) {
    			part = new OPMObjectProcessDiagramEditPart();
    		} else if (model instanceof OPMObject) {
    			part = new OPMObjectEditPart();
    		} else if (model instanceof OPMProcess) {
    			part = new OPMProcessEditPart();
    		} else if (model instanceof OPMProceduralLink) {
    			// It is important for OPMProceduralLink to be before OPMLink because
    			// they have an is-a relation and we would get the wrong EditPart.
    			part = new OPMProceduralLinkEditPart();
    		} else if (model instanceof OPMLink) {
    			part = new OPMLinkEditPart();
    		} else if (model instanceof OPMStructuralLinkAggregator) {
    		    part = new OPMStructuralLinkAggregatorEditPart();
    		} else {
    			throw new IllegalArgumentException("Model class "+model.getClass()+" not supported yet.");
    		}
    
    		if (part != null) {
    			part.setModel(model);
    		}
    
    		return part;
    	}
    }
    
  5. After all of this work, we can fire up our editor and add/delete some structural links between things in the diagram. Note that because of the refactoring we did in the model, the editor will not be able to open the file that exists in your workspace, so remove it and create a new one. These are the results in my editor:

We still have some work left, like changing the router of the links that start and end at the aggregator that must be a ManhattanConnectionRouter and not a BendpointConnectionRouter as currently implemented, but this will have to wait.

You can find the final project files here.

Hope you enjoyed the tutorial. If you have any comments, please send them. Feedback is always good, and can always improve my work.

Next Tutorial: Creating an OPM GEF Editor – Part 16: Displaying EMF Properties in a GEF Editor.