Vainolo's Blog

Creating a GEF Editor – Part 7: Moving Elements and Direct Editing

11 comments

Previous Tutorial: Creating a GEF Editor – Part 6: Model Refactoring and Editing Diagram Entities

In today’s tutorial we will continue adding more editing capabilities to our GEF editor – we’ll be able to move graph entities and to edit their content directly (so we can change the annoying “<…>” text that is currently shown on them). For dessert we’ll do some magic so that direct editing is enabled right after a new entity is added to the diagram. This is going to be easier and faster than the previous tutorial, so lets get going!.

As usual, the initial project files can be found here.

  1. We are first going to implement moving and resizing diagram entities, specifically the OPMObject and OPMProcess entities that children of the OPMObjectProcessDiagram. As usual, editing operations are done by creating edit policies on the desired EditPart. The EditPolicy then creates and executes a Command which alter the model and (if we did things the right way), the EditPart gets notified of model changes and updates the view respectively.
  2. We create a new command that can be applied to OPMThing, which changes their constraints (location/size) when executed:
    package com.vainolo.phd.opm.gef.editor.command;
    
    import com.vainolo.phd.opm.model.OPMThing;
    
    import org.eclipse.draw2d.geometry.Rectangle;
    import org.eclipse.gef.commands.Command;
    
    public class OPMThingChangeConstraintCommand extends Command {
    
    	private Rectangle oldConstraint;
    	private Rectangle newConstraint;
    	private OPMThing model;
    	
    	@Override public void execute() {
    		if(oldConstraint == null) {
    			oldConstraint = model.getConstraints();
    		}
    		model.setConstraints(newConstraint);
    	}
    
    	@Override public void undo() {
    		model.setConstraints(oldConstraint);
    	}
    
    	public void setModel(OPMThing model) {
    		this.model = model;
    	}
    	
    	public void setNewConstraint(Rectangle newConstraint) {
    		this.newConstraint = newConstraint;
    	}
    }
    
  3. Override the createChangeConstraintCommand method of the OPMObjectProcessDiagramXYLayoutPolicy to create the new command when requested. Note that the policy is part of the OPMObjectProcessDiagramEditPart and not part of the OPMThingEditPart:
    package com.vainolo.phd.opm.gef.editor.policy;
    
    import com.vainolo.phd.opm.model.OPMObject;
    import com.vainolo.phd.opm.model.OPMObjectProcessDiagram;
    import com.vainolo.phd.opm.model.OPMProcess;
    import com.vainolo.phd.opm.model.OPMThing;
    
    import org.eclipse.draw2d.geometry.Rectangle;
    import org.eclipse.gef.EditPart;
    import org.eclipse.gef.commands.Command;
    import org.eclipse.gef.editpolicies.XYLayoutEditPolicy;
    import org.eclipse.gef.requests.CreateRequest;
    
    import com.vainolo.phd.opm.gef.editor.command.OPMThingCreateCommand;
    import com.vainolo.phd.opm.gef.editor.command.OPMThingChangeConstraintCommand;
    
    /**
     * This class describes the commands that can be used to change the layout inside the
     * ObjectProcessDiagram, Create new entities inside the ObjectProcessDiagram
     * 
     * @author vainolo
     *
     */
    public class OPMObjectProcessDiagramXYLayoutPolicy extends XYLayoutEditPolicy {
    
    	/**
    	 * Command created the user requests to change the constraint (size, location) of an object that is
    	 * part of an OPD.
    	 */
    	@Override protected Command createChangeConstraintCommand(EditPart child, Object constraint) {
    		OPMThingChangeConstraintCommand command = new OPMThingChangeConstraintCommand();
    		command.setModel((OPMThing) child.getModel());
    		command.setNewConstraint((Rectangle) constraint);
    		return command;
    	}
    
    	/**
    	 * Command created to add new things to the OPD.
    	 */
    	@Override protected Command getCreateCommand(CreateRequest request) {
    		Command retVal = null;
    		if(request.getNewObjectType().equals(OPMObject.class) || request.getNewObjectType().equals(OPMProcess.class)) {
    			OPMThingCreateCommand command = new OPMThingCreateCommand();
    			command.setLocation(request.getLocation());
    			command.setParent((OPMObjectProcessDiagram)(getHost().getModel()));
    			command.setThing((OPMThing)(request.getNewObject()));
    			retVal = command;
    		} 
    		return retVal;
    	}
    }
    
  4. To finish the job, we must now listen to changes in the OPMThing model in the OPMThingEditPart and repaint the view when required:
    package com.vainolo.phd.opm.gef.editor.part;
    
    import org.eclipse.emf.common.notify.Adapter;
    import org.eclipse.emf.common.notify.Notification;
    import org.eclipse.emf.common.notify.Notifier;
    import org.eclipse.gef.editparts.AbstractGraphicalEditPart;
    
    import com.vainolo.phd.opm.gef.editor.figure.OPMThingFigure;
    import com.vainolo.phd.opm.model.OPMThing;
    
    public abstract class OPMThingEditPart extends AbstractGraphicalEditPart {
    
    	private OPMThingAdapter adapter;
    	
    	public OPMThingEditPart() {
    		super();
    		adapter = new OPMThingAdapter();
    	}
    	
    	@Override protected void refreshVisuals() {
    		OPMThingFigure figure = (OPMThingFigure)getFigure();
    		OPMThing model = (OPMThing)getModel();
    		OPMObjectProcessDiagramEditPart parent = (OPMObjectProcessDiagramEditPart) getParent();
    		
    		figure.getNameLabel().setText(model.getName());
    		parent.setLayoutConstraint(this, figure, model.getConstraints());
    	}
    	
    	@Override public void activate() {
    		if(!isActive()) {
    			((OPMThing)getModel()).eAdapters().add(adapter);
    		}
    		super.activate();
    	}
    
    	@Override public void deactivate() {
    		if(isActive()) {
    			((OPMThing)getModel()).eAdapters().remove(adapter);
    		}
    
    		super.deactivate();
    	}	
    	
    	public class OPMThingAdapter implements Adapter {
    
    		// Adapter interface
    		@Override public void notifyChanged(Notification notification) {
    			refreshVisuals();
    		}
    
    		@Override public Notifier getTarget() {
    			return (OPMThing)getModel();
    		}
    
    		@Override public void setTarget(Notifier newTarget) {
    			// Do nothing.
    		}
    
    		@Override public boolean isAdapterForType(Object type) {
    			return type.equals(OPMThing.class);
    		}
    	}	
    }
    
  5. That’s all you need to move and re-size diagram entities. You can execute the plug-in and move things around now.
  6. Direct editing of diagram entities is a bit more complicated, but just a bit. We’ll be needing a Command and a EditPolicy as before, but on top of this we need two more things: a DirectEditManager which is in charge of creating and maintaining an editor on which editing is executing, and a CellEditorLocator which tells the manager where to locate the cell editor for the EditPart. Let’s start by creating the Command and the EditPolicy:
    package com.vainolo.phd.opm.gef.editor.command;
    
    import org.eclipse.gef.commands.Command;
    
    import com.vainolo.phd.opm.model.OPMThing;
    
    public class OPMThingRenameCommand extends Command {
    	
    	private String oldName, newName;
    	private OPMThing model;
    
    	@Override public void execute() {
    		oldName = model.getName();
    		model.setName(newName);
    	}
    
    	@Override public void undo() {
    		model.setName(oldName);
    	}
    	
    	public void setNewName(String newName) {
    		this.newName = newName;
    	}
    	
    	public void setModel(OPMThing model) {
    		this.model = model;
    	}
    }
    
    package com.vainolo.phd.opm.gef.editor.policy;
    
    import org.eclipse.gef.commands.Command;
    import org.eclipse.gef.editpolicies.DirectEditPolicy;
    import org.eclipse.gef.requests.DirectEditRequest;
    
    import com.vainolo.phd.opm.gef.editor.command.OPMThingRenameCommand;
    import com.vainolo.phd.opm.gef.editor.figure.OPMThingFigure;
    import com.vainolo.phd.opm.model.OPMThing;
    
    public class OPMThingDirectEditPolicy extends DirectEditPolicy {
    
    	@Override protected Command getDirectEditCommand(DirectEditRequest request) {
    		OPMThingRenameCommand command = new OPMThingRenameCommand();
    		command.setModel((OPMThing) getHost().getModel());
    		command.setNewName((String) request.getCellEditor().getValue());
    		return command;
    	}
    
    	@Override protected void showCurrentEditValue(DirectEditRequest request) {
    		String value = (String) request.getCellEditor().getValue();
    		((OPMThingFigure)getHostFigure()).getNameLabel().setText(value);		
    	}
    }
    
  7. The CellEditorLocator used to edit the OPMThing is passed the Label on which the name of the OPMThing is displayed, and calculates the location where the editor should be set. Examining the code you can see that the location and size of the editor is defined by the text that is currently displayed on the figure and not on the complete size of the Label that displays it (which BTW is the full size of the figure. The text is displayed just centered by default):
    package com.vainolo.phd.opm.gef.editor.part;
    
    import org.eclipse.draw2d.Label;
    import org.eclipse.draw2d.geometry.Rectangle;
    import org.eclipse.gef.tools.CellEditorLocator;
    import org.eclipse.jface.viewers.CellEditor;
    import org.eclipse.swt.SWT;
    import org.eclipse.swt.graphics.Point;
    import org.eclipse.swt.widgets.Text;
    
    public class OPMThingCellEditorLocator implements CellEditorLocator {
    
    	private Label nameLabel;
    	
    	public OPMThingCellEditorLocator(Label label) {
    		this.nameLabel = label;
    	}
    
    	@Override public void relocate(CellEditor celleditor) {
    		Text text = (Text) celleditor.getControl();
    		Point pref = text.computeSize(SWT.DEFAULT, SWT.DEFAULT);
    		Rectangle rect = nameLabel.getTextBounds().getCopy();
    		nameLabel.translateToAbsolute(rect);
    		text.setBounds(rect.x - 1, rect.y - 1, pref.x + 1, pref.y + 1);		
    	}
    }
    
  8. A DirectEditManager manages the connection between the CellEditor, the CellEditorLocator and the environment, for example extending and moving the editor as the text in it gets larger. When the editing finished, the DirectEditManaged executed the direct edit command:
    package com.vainolo.phd.opm.gef.editor.part;
    
    import org.eclipse.draw2d.Label;
    import org.eclipse.gef.GraphicalEditPart;
    import org.eclipse.gef.tools.CellEditorLocator;
    import org.eclipse.gef.tools.DirectEditManager;
    
    public class OPMThingDirectEditManager extends DirectEditManager {
    
    	Label label;
    	
    	public OPMThingDirectEditManager(GraphicalEditPart source, Class editorType, CellEditorLocator locator, Label label) {
    		super(source, editorType, locator);
    		this.label = label;
    	}
    
    	@Override protected void initCellEditor() {
    		String initialLabelText = label.getText();
    		getCellEditor().setValue(initialLabelText);
    	}
    }
    
  9. We have the command, we have the policy, we have the manager and we have the locator. Now install the newly created EditPolicy in the OPMThingEditPart and that should be it, right? well no, for a reason I have yet to understand fully (and there is always a good reason), installing the DirectEditPolicy on an EditPart only causes direct edit request to be passed directly to the performRequest method of the EditPart. This is probably done so that the developer can decide how to do direct edits and not to constraint him to use managers and locators. Because of this, we must modify the OPMThingEditPart to handle these direct edit requests:
    package com.vainolo.phd.opm.gef.editor.part;
    
    import org.eclipse.draw2d.Label;
    import org.eclipse.emf.common.notify.Adapter;
    import org.eclipse.emf.common.notify.Notification;
    import org.eclipse.emf.common.notify.Notifier;
    import org.eclipse.gef.EditPolicy;
    import org.eclipse.gef.Request;
    import org.eclipse.gef.RequestConstants;
    import org.eclipse.gef.editparts.AbstractGraphicalEditPart;
    import org.eclipse.jface.viewers.TextCellEditor;
    
    import com.vainolo.phd.opm.gef.editor.figure.OPMThingFigure;
    import com.vainolo.phd.opm.gef.editor.policy.OPMThingDirectEditPolicy;
    import com.vainolo.phd.opm.model.OPMThing;
    
    public abstract class OPMThingEditPart extends AbstractGraphicalEditPart {
    
    	private OPMThingAdapter adapter;
    	
    	public OPMThingEditPart() {
    		super();
    		adapter = new OPMThingAdapter();
    	}
    	
    	@Override protected void createEditPolicies() {
    		installEditPolicy(EditPolicy.DIRECT_EDIT_ROLE, new OPMThingDirectEditPolicy());
    	}
    
    	@Override public void performRequest(Request req) {
    		if(req.getType() == RequestConstants.REQ_DIRECT_EDIT) {
    			performDirectEditing();
    		}
    	}
    	
    	private void performDirectEditing() {
    		Label label = ((OPMThingFigure)getFigure()).getNameLabel();
    		OPMThingDirectEditManager manager = new OPMThingDirectEditManager(this, TextCellEditor.class, new OPMThingCellEditorLocator(label), label);
    		manager.show();
    	}		
    	
    	@Override protected void refreshVisuals() {
    		OPMThingFigure figure = (OPMThingFigure)getFigure();
    		OPMThing model = (OPMThing)getModel();
    		OPMObjectProcessDiagramEditPart parent = (OPMObjectProcessDiagramEditPart) getParent();
    		
    		figure.getNameLabel().setText(model.getName());
    		parent.setLayoutConstraint(this, figure, model.getConstraints());
    	}
    	
    	@Override public void activate() {
    		if(!isActive()) {
    			((OPMThing)getModel()).eAdapters().add(adapter);
    		}
    		super.activate();
    	}
    
    	@Override public void deactivate() {
    		if(isActive()) {
    			((OPMThing)getModel()).eAdapters().remove(adapter);
    		}
    
    		super.deactivate();
    	}	
    	
    	public class OPMThingAdapter implements Adapter {
    
    		// Adapter interface
    		@Override public void notifyChanged(Notification notification) {
    			refreshVisuals();
    		}
    
    		@Override public Notifier getTarget() {
    			return (OPMThing)getModel();
    		}
    
    		@Override public void setTarget(Notifier newTarget) {
    			// Do nothing.
    		}
    
    		@Override public boolean isAdapterForType(Object type) {
    			return type.equals(OPMThing.class);
    		}
    	}	
    }
    
  10. And for the finishing touch, and for being a lazy programmer and not editing automatically generated code, we also have to remove the createEditPolicies method from the OPMObjectEditPart and OPMProcessEditPart, otherwise the method in the OPMThingEditPart will never be called:
    package com.vainolo.phd.opm.gef.editor.part;
    
    import org.eclipse.draw2d.IFigure;
    
    import com.vainolo.phd.opm.gef.editor.figure.OPMObjectFigure;
    
    public class OPMObjectEditPart extends OPMThingEditPart {
    
    	@Override 
    	protected IFigure createFigure() {
    		return new OPMObjectFigure();
    	}
    }
    
    package com.vainolo.phd.opm.gef.editor.part;
    
    import org.eclipse.draw2d.IFigure;
    
    import com.vainolo.phd.opm.gef.editor.figure.OPMProcessFigure;
    
    public class OPMProcessEditPart extends OPMThingEditPart {
    
    	@Override protected IFigure createFigure() {
    		return new OPMProcessFigure();
    	}
    }
    
  11. Direct label editing should now work on your editor. To apply it, select the figure and then click once more (leaving some time between the clicks).
  12. As a finishing touch for this tutorial, we’ll customize our editor so that after a user adds a new entity to the diagram, the editor automatically activates the direct editing of the entity’s name. It took me some time to find this code on the internet and I had to improve it a bit so that it looked good. Basically what we want to do is to send a DirectEditRequest to the OPMThingEditPart right after it is created and displayed on the diagram… so where can we hook this? Turns out that the recommended way to do it (as show in the eclipse GEF forum) is to extend the CreationTool we used to create the EditPart and to send the request after the tool creates the new edit part. So we need to create a new class:
    package com.vainolo.phd.opm.gef.editor.tool;
    
    import org.eclipse.gef.EditPart;
    import org.eclipse.gef.EditPartViewer;
    import org.eclipse.gef.Request;
    import org.eclipse.gef.requests.DirectEditRequest;
    import org.eclipse.gef.tools.CreationTool;
    import org.eclipse.swt.widgets.Display;
    
    public class CreationAndDirectEditTool extends CreationTool {
    
    	@Override protected void performCreation(int button) {
    		super.performCreation(button);
    		
    		EditPartViewer viewer = getCurrentViewer();
    		final Object model = getCreateRequest().getNewObject();
    		if (model == null || viewer == null) {
    			return;
    		}
    		
    		final Object o = getCurrentViewer().getEditPartRegistry().get(model);
    		if(o instanceof EditPart) {
    			Display.getCurrent().asyncExec(new Runnable() {
    				
    				@Override public void run() {
    					EditPart part = (EditPart)o;
    					Request request = new DirectEditRequest();
    					part.performRequest(request);
    				}
    			});
    		}
    	}
    }
    

    and use this class in the editor’s palette instead of the default tool used by the CreationToolEntry of our pallete:

    package com.vainolo.phd.opm.gef.editor;
    
    import org.eclipse.gef.palette.CreationToolEntry;
    import org.eclipse.gef.palette.PaletteGroup;
    import org.eclipse.gef.palette.PaletteRoot;
    import org.eclipse.gef.palette.SelectionToolEntry;
    
    import com.vainolo.phd.opm.gef.editor.factory.OPMObjectFactory;
    import com.vainolo.phd.opm.gef.editor.factory.OPMProcessFactory;
    import com.vainolo.phd.opm.gef.editor.tool.CreationAndDirectEditTool;
    
    public class OPMGraphicalEditorPalette extends PaletteRoot {
    
    	PaletteGroup group;
    	
    	public OPMGraphicalEditorPalette() {
    		addGroup();
    		addSelectionTool();
    		addOPMObjectTool();
    		addOPMProcessTool();
    	}
    	
    	private void addSelectionTool() {
    		SelectionToolEntry entry = new SelectionToolEntry();
    		group.add(entry);
    		setDefaultEntry(entry);
    	}
    	
    	private void addGroup() {
    		group = new PaletteGroup("OPM Controls");
    		add(group);
    	}
    	
    	private void addOPMObjectTool() {
    		CreationToolEntry entry = new CreationToolEntry("OPMObject", "Create a new Object", new OPMObjectFactory(), null, null);
    		entry.setToolClass(CreationAndDirectEditTool.class);		
    		group.add(entry);
    	}
    	
    	private void addOPMProcessTool() {
    		CreationToolEntry entry = new CreationToolEntry("OPMProcess", "Create a new Process", new OPMProcessFactory(), null, null);
    		entry.setToolClass(CreationAndDirectEditTool.class);		
    		group.add(entry);
    	}
    }
    
  13. Execute your editor and add a new OPMObject or OPMProcess to the diagram. Nice, ah?. You are probably wondering what improvement I made to the code, so I’ll tell you my secret :-): In all of the code examples I found on the internet the DirectEditRequest was sent in the same tread that executed the creation, which I think is not the GUI thread on which events and requests should be handled. What happens is that the direct edit cell appears at the origin of the newly created figure because the figure’s constraints are not initiallized correctly. What I did is to send the Request to the EditPart from inside the GUI thread (using Display.getCurrent().asyncExec()), so the request arrives in the correct order. How dificult it is not to program by coincidece, it gets us all the time.

Hope you enjoyed this tutorial. As usual, the completed project files can be found here.

See you next time.

Next Tutorial: Creating a GEF Editor – Part 8: Delete, Undo and Redo

Written by vainolo

July 7th, 2011 at 4:04 pm

Posted in Programming

Tagged with , , ,

11 Responses to 'Creating a GEF Editor – Part 7: Moving Elements and Direct Editing'

Subscribe to comments with RSS or TrackBack to 'Creating a GEF Editor – Part 7: Moving Elements and Direct Editing'.

  1. There is a problem with celleditor size. If one starts name with Z or W symbol, then the symbol will be cut by clipping rectangle

    Leo

    30 Apr 13 at 03:00

  2. Hi Leo. I don’t understand the problem. Can you please expand the comment so that I can check it out?

    vainolo

    2 May 13 at 22:56

  3. If you try to enter something like “WWWW” then first “W” will be eaten by the clipping.procedure.

    Leo

    4 May 13 at 03:56

  4. Yea, I also saw something like that happening. I ignored is :-). Do you know why it happens?

    vainolo

    4 May 13 at 22:22

  5. Hallo vainolo,

    how to remove the createEditPolicies method from the OPMObjectEditPart and OPMProcessEditPart? There is an error showed in my eclipse.

    jj

    15 May 13 at 14:36

  6. What do you mean “there is an error”? If you want help, please be specific and explain what you want to do, why, and what isn’t working.

    vainolo

    16 May 13 at 00:43

  7. Hello, love the GEF tutorials.. thank you. One quick question however, I am getting a red ‘not allowed’ symbol when I click on an object from the palette (e.g. process) and hover over the canvas. I am not allowed to place a model object on the canvas however I can select them from the palette. Any idea what is wrong or tips I can take to fix the issue?

    Joe

    2 Sep 13 at 11:46

  8. Are you using my code or your own code? Maybe you didn’t implement all the required methods… You need a tool, a factory and a policy that can be applied to the model.

    vainolo

    15 Sep 13 at 20:52

  9. Thank you very much VAINOLO. Your tutorial helped me to implement an object with editable labels and to understand how GEF works.

    Daniel Kim

    26 Dec 13 at 02:42

  10. Thank you Vainolo for the excellent tutorial, it helps me a lot.

    A small comment on OPMThingEditPart class, It should be not as abstract class.

    Thanks
    Gap

    Gap

    6 Jun 14 at 02:02

  11. Why not? I never instantiate a thing. I only instantiate OPMObjects and OPMProcesses.

    vainolo

    8 Jun 14 at 08:06

Leave a Reply

%d bloggers like this: