Creating an OPM GEF Editor – Part 20: Creating a Context Menu and Adding Custom Actions

Previous Tutorial: Creating an OPM GEF Editor – Part 19: Displaying Tooltips.

Wow. It’s been almost two weeks since my last post on this subject. I’ve been really busy preparing lectures for the upcoming semester – I’m teaching an undergraduate course on “Information Systems Design and Analysis” at the Technion‘s Industrial Engineering faculty. Almost two weeks and just yesterday I finally finished preparing the first lecture. I guess the number of picture and effects will be greatly reduced on the following lectures or I wont be able to prepare the slides on time. Anyway, taking a break from this, this tutorial show something I learned a bit before starting to work on the lectures but was unable to post until now: how to create a context menu in the diagram, and how to add new custom actions that are activated by items in the menu.

You can download the initial project files from here so that you can follow things step by step (hopefully).

  1. First thing we are going to do is add a context menu to our editor and populate it with actions that already exist in out editor (undo and redo). A context menu is implemented by creating a ContextMenuProvider which manages the menu’s contents and then setting this provider in the editor. The new ContextMenuProvider has to implement the buildContextMenu method to calculate which menu items should be shown in the menu. For some reason that is still unknown to me, the menu’s contents must be calculated every time the menu is shown. The first implementation creates a new menu which is filled with a set of GEF standard groups and after this two actions are added to the menu: undo and redo.
    package com.vainolo.phd.opm.gef.editor;
    
    import org.eclipse.gef.ContextMenuProvider;
    import org.eclipse.gef.EditPartViewer;
    import org.eclipse.gef.ui.actions.ActionRegistry;
    import org.eclipse.gef.ui.actions.GEFActionConstants;
    import org.eclipse.jface.action.IAction;
    import org.eclipse.jface.action.IMenuManager;
    import org.eclipse.ui.actions.ActionFactory;
    
    public class OPMGraphicalEditorContextMenuProvider extends ContextMenuProvider {
    
        private ActionRegistry actionRegistry;
    
        public OPMGraphicalEditorContextMenuProvider(EditPartViewer viewer, final ActionRegistry actionRegistry) {
            super(viewer);
            setActionRegistry(actionRegistry);
        }
    
        @Override
        public void buildContextMenu(IMenuManager menu) {
            GEFActionConstants.addStandardActionGroups(menu);
    
            IAction action;
    
            action = getActionRegistry().getAction(ActionFactory.UNDO.getId());
            menu.appendToGroup(GEFActionConstants.GROUP_UNDO, action);
            action = getActionRegistry().getAction(ActionFactory.REDO.getId());
            menu.appendToGroup(GEFActionConstants.GROUP_UNDO, action);
        }
    
        private ActionRegistry getActionRegistry() {
            return actionRegistry;
        }
    
        private void setActionRegistry(final ActionRegistry actionRegistry) {
            this.actionRegistry = actionRegistry;
        }
    }
    
  2. We now plug in the menu into the editor in the editor’s configureGraphicalViewer method:
        @Override
        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()));
        }
    

    Fire up the application and you can see the results right away:

    Notice that the Redo menu item is inactive, and that is because the action that is associated with this menu item is not enabled (more on this later).

  3. Now lets create the new menu item to automatically resize our Things. A menu item is connected to an Action that is executed when the item is clicked. The GEF framework provides us with some action implementations, in this case the SelectionAction. This class which provides us with some helper methods to do our job, for example the getSelectedObjects method, so our implementation will be based on this class. The implementation is pretty straightforward:
    package com.vainolo.phd.opm.gef.action;
    
    import java.util.List;
    import org.eclipse.gef.EditPart;
    import org.eclipse.gef.Request;
    import org.eclipse.gef.commands.CompoundCommand;
    import org.eclipse.gef.ui.actions.SelectionAction;
    import org.eclipse.ui.IWorkbenchPart;
    import com.vainolo.phd.opm.gef.editor.part.OPMNodeEditPart;
    
    /**
     * An action that executes the command returned by the selected {@link EditPart} instances when given a
     * {@link ResizeToContentsAction#REQ_RESIZE_TO_CONTENTS}.
     * @author vainolo
     */
    public class ResizeToContentsAction extends SelectionAction {
    
        public static final String RESIZE_TO_CONTENTS = "ResizeToContents";
        public static final String REQ_RESIZE_TO_CONTENTS = "ResizeToContents";
    
        Request request;
    
        /**
         * Create a new instance of the class.
         * @param part
         */
        public ResizeToContentsAction(IWorkbenchPart part) {
            super(part);
            setId(RESIZE_TO_CONTENTS);
            setText("Resize to Contents");
            request = new Request(REQ_RESIZE_TO_CONTENTS);
        }
    
        /**
         * Execute the commands that perform the {@link ResizeToContentsAction#REQ_RESIZE_TO_CONTENTS REQ_RESIZE_TO_CONTENTS}.
         *
         * It is assumed that this method is executed directly after
         * {@link ResizeToContentsAction#calculateEnabled() calculateEnabled()}
         */
        @Override
        public void run() {
            // selected objects must be nodes because the action is enabled.
            @SuppressWarnings("unchecked") List<OPMNodeEditPart> editParts = getSelectedObjects();
            CompoundCommand compoundCommand = new CompoundCommand();
            for(OPMNodeEditPart nodeEditPart : editParts) {
                compoundCommand.add(nodeEditPart.getCommand(request));
            }
            execute(compoundCommand);
        }
    
        /**
         * {@inheritDoc}
         * <p>The action is enabled if all the selected entities on the
         * editor are {@link OPMNodeEditPart} instances</p>
         */
        @Override
        protected boolean calculateEnabled() {
            if(getSelectedObjects().isEmpty()) {
                return false;
            }
            for(Object selectedObject : getSelectedObjects()) {
                if(!(selectedObject instanceof OPMNodeEditPart)) {
                    return false;
                }
            }
            return true;
        }
    }
    

    You may have noticed something strange… where do you actually re-size the figures? Here’s the trick: see the noteEditPart.getCommand(request) on line 48? This is where the magic occurs. When an EditPart executes this method, it iterates over all of the EditPolicy instances that have been installed in the EditPart to see if there is one which can handle the given request. So what we have to do now is either create a new EditPolicy which only works on this request (not a good practice) or extend an existing policy that is already installed in a OPMNodeEditPart. We have selected to extend the OPMNodeComponentEditPolicy as follows:

    package com.vainolo.phd.opm.gef.editor.policy;
    
    import org.eclipse.draw2d.geometry.Dimension;
    import org.eclipse.draw2d.geometry.Rectangle;
    import org.eclipse.gef.EditPolicy;
    import org.eclipse.gef.Request;
    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.action.ResizeToContentsAction;
    import com.vainolo.phd.opm.gef.editor.command.OPMNodeChangeConstraintCommand;
    import com.vainolo.phd.opm.gef.editor.command.OPMNodeDeleteCommand;
    import com.vainolo.phd.opm.gef.editor.figure.OPMNodeFigure;
    import com.vainolo.phd.opm.gef.editor.part.OPMNodeEditPart;
    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 {
    
        private static final int INSETS = 20;
    
        /**
         * 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;
            compoundCommand = createRecursiveDeleteNodeCommand(nodeToDelete);
    
            return compoundCommand;
        }
    
        /**
         * This function creates a command that consists of all the commands
         * required to delete the given node and all of the nodes contained inside it.
         * This function is called recursively when a node is a container and has internal nodes.
         * @param nodeToDelete the node that will be deleted.
         * @return a {@link CompoundCommand} command that deletes the node, the contained nodes
         * and all links that must be deleted.
         */
        private CompoundCommand createRecursiveDeleteNodeCommand(OPMNode nodeToDelete) {
            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);
                }
            }
    
            for(OPMNode node : nodeToDelete.getNodes()) {
                Command containedNodeDelete = createRecursiveDeleteNodeCommand(node);
                compoundCommand.add(containedNodeDelete);
            }
    
            // Create a command to delete the node.
            OPMNodeDeleteCommand nodeDeleteCommand = new OPMNodeDeleteCommand();
            nodeDeleteCommand.setNode(nodeToDelete);
            compoundCommand.add(nodeDeleteCommand);
    
            return compoundCommand;
        }
    
        /**
         * Create a command to resize a node based on the current contents of the node.
         * The current implementation uses the figure's {@link OPMNodeFigure#getPreferredSize()} to
         * calculate this size.
         *
         * @return
         */
        private OPMNodeChangeConstraintCommand createResizeToContentsCommand() {
            OPMNodeEditPart host = (OPMNodeEditPart) getHost();
            OPMNode node = (OPMNode) host.getModel();
            OPMNodeFigure figure = (OPMNodeFigure) host.getFigure();
    
            // We assume the node's preferred size includes all of its contents.
            Dimension preferredSize = figure.getPreferredSize();
            preferredSize.expand(INSETS, INSETS);
            Rectangle newConstraints = node.getConstraints().getCopy();
            newConstraints.setWidth(preferredSize.width);
            newConstraints.setHeight(preferredSize.height);
    
            OPMNodeChangeConstraintCommand command = new OPMNodeChangeConstraintCommand();
            command.setNode(node);
            command.setNewConstraint(newConstraints);
            return command;
        }
    
        /**
         * <p>Extends the parent implementation by handling incoming REQ_RESIZE_TO_CONTENTS requests.</p>
         * <p>The parent implementation {@inheritDoc}</p>
         */
        @Override
        public Command getCommand(Request request) {
            if(request.getType().equals(ResizeToContentsAction.REQ_RESIZE_TO_CONTENTS)) {
                return createResizeToContentsCommand();
            }
            return super.getCommand(request);
        }
    }
    
  4. In the code above we used the getPreferredSize to calculate ther desired size of the figure, and since we want this to be the size of the figure’s label, we have to override the method in our OPMThingFigure:
        /**
         * The thing's preferred size is the size of its name label.
         */
        @Override
        public Dimension getPreferredSize(int wHint, int hHint) {
            Dimension d = new Dimension();
            Rectangle textRectangle = getNameLabel().getTextBounds().getCopy();
            d.width = textRectangle.width;
            d.height = textRectangle.height;
            return d;
        }
    
  5. Now we add the new action to our context menu:
        @Override
        public void buildContextMenu(IMenuManager menu) {
            GEFActionConstants.addStandardActionGroups(menu);
    
            IAction action;
    
            action = getActionRegistry().getAction(ActionFactory.UNDO.getId());
            menu.appendToGroup(GEFActionConstants.GROUP_UNDO, action);
            action = getActionRegistry().getAction(ActionFactory.REDO.getId());
            menu.appendToGroup(GEFActionConstants.GROUP_UNDO, action);
            action = getActionRegistry().getAction(ResizeToContentsAction.RESIZE_TO_CONTENTS);
            menu.appendToGroup(GEFActionConstants.GROUP_EDIT, action);
        }
    
  6. And to finish things up, we have to register the action in the editor. We do this by overriding the createActions method in the OPMGraphicalEditor:
    
        @Override
        protected void createActions() {
            ResizeToContentsAction action = new ResizeToContentsAction(this);
            getActionRegistry().registerAction(action);
            getSelectionActions().add(action.getId());
    
            super.createActions();
        }
    
  7. That’s all! Execute your editor and automatically re-size your figures.

    Works nice, doesn’t it?

We could expand the implementation so that the menu item is only visible when there are only EditPart instances selected (instead of just being disabled) but I’ll leave that to you as a homework. See you next class :-).

The final project files can be found here. The files may be outdated so please check the comments for required fixes.

Next Tutorial: Creating an OPM GEF Editor – Part 21: Adding Keyboard Shortcuts

7 thoughts on “Creating an OPM GEF Editor – Part 20: Creating a Context Menu and Adding Custom Actions

  1. Hello Vainolo,

    I have seen your tutorials for GEF Its look simple and great. Can you post tutorial for Cut,copy and paste actions in eclipse gef editors. FYI you already solved my problem in gef about connections posted in stack overflow.

    Thanks.

    • Good to hear that my tutorials and answers have helped. I’m planning a c&p tutorial in the future but I’ve been short of time lately.
      BTW, just curious, did you mark my answer in stackoverflow as the correct answer?

  2. Hello Vainolo,

    I already marked your answer .This is for your kind information.

  3. I think OPMNodeImpl line 250 should be:
    OPMNode containerNode = (OPMNode) currentContainer;
    rather then
    OPMNode containerNode = (OPMNode) getContainer;

    otherwise you get infinite loop when connecting to an object within an object within an object.

  4. Hi Vainolo,

    First, I want to say a big thank to you for your tutorials.

    For this tutorial, I do what you touch carefully, but my menu items are always disable. I found that the getSelectedObjects() method in my PropertiesSelectionAction (subclassed from SelectionAction) is always empty!?
    I believe that I missed something, but I couldn’t point it out.

    My editor is subclassed from GraphicalEditorWithFlyoutPalette, and overwrite createActions()

    action = new PropertiesSelectionAction((IWorkbenchPart)this);
    actionRegistry.registerAction(action);
    getSelectionActions().add(action.getId());

    Please give me a solution,
    Thanks,

    • Hi Dao. The question you are asking is not about my tutorial, since I don’t have a PropertiesSelectionAction. What changes are you doing? Without more information I cannot help you.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.