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).
- 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 newContextMenuProvider
has to implement thebuildContextMenu
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; } }
- 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). - 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 theSelectionAction
. This class which provides us with some helper methods to do our job, for example thegetSelectedObjects
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 anEditPart
executes this method, it iterates over all of theEditPolicy
instances that have been installed in theEditPart
to see if there is one which can handle the given request. So what we have to do now is either create a newEditPolicy
which only works on this request (not a good practice) or extend an existing policy that is already installed in aOPMNodeEditPart
. We have selected to extend theOPMNodeComponentEditPolicy
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); } }
- 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 ourOPMThingFigure
:/** * 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; }
- 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); }
- And to finish things up, we have to register the action in the editor. We do this by overriding the
createActions
method in theOPMGraphicalEditor
:@Override protected void createActions() { ResizeToContentsAction action = new ResizeToContentsAction(this); getActionRegistry().registerAction(action); getSelectionActions().add(action.getId()); super.createActions(); }
- 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