站内搜索: 请输入搜索关键词

当前页面: 开发资料首页Eclipse 英文资料A Shape Diagram Editor

A Shape Diagram Editor

摘要: Graphical Editing Framework (GEF) provides a powerful foundation for creating editors for visual editing of arbitrary models. Its effectiveness lies in a modular build, fitting use of design patterns, and decoupling of components that comprise a full, working editor. To a newcomer, the sheer number and variety of concepts and techniques present in GEF may feel intimidating. However, once learned and correctly used, they help to develop highly scalable and easy to maintain software. This article aims to provide a gentle yet comprehensive introduction to GEF. It describes a shape diagram editor - a small, fully functional test case of core concepts. By Bo Majewski, Cisco Systems, Inc.


Graphical Editing Framework (GEF) has been designed to allow editing of user data, generally referred to as the model, using graphical rather than textual format. It becomes an invaluable tool when dealing with entities that contain many-to-many, one-to-many and other complex relationships. With the popularity of the Eclipse Rich Client Platform, which leads to development of editors for more than just code, the importance of GEF is certain to increase. A few existing examples, such as database schema editor [7], logical circuits editor, and a task flow manager nicely illustrate both the power and flexibility of the framework that may be applied to such varied and disparate domains.

Yet the trouble with any generic framework, and GEF is no exception, is that its comprehensive design makes it hard to learn. Until recently, the smallest available example came with over 75 classes. Trying to understand nuances of GEF from interaction of that many user defined types and hundreds more native to GEF is certain to test the patience and acumen of even the most diligent developers. To rectify this issue, a new, much smaller example editor has been added and will appear in the upcoming 3.1 release. The shape diagram editor (see Figure 1) allows you to create and edit simple diagrams. It manipulates two types of objects, represented by rectangles and ellipses. You may connect any two objects with one of the two connection types. The two connection types are represented by solid and dashed lines. Each connection is directed, in the sense that it starts at a source object and terminates in the target object. The direction of each connection is indicated by an arrow. A connection may be reattached by dragging its source or target to a new object. Objects in the editor may be selected either by clicking on them or by dragging a rubber band around them. Selected objects can be deleted. All model manipulations, such as adding or deleting objects, moving them, resizing them, etc., may be undone or redone. Finally, the editor integrates with the Properties and Outline standard views of Eclipse. The editor's virtue comes not from its usefulness, but rather from the fact its limited number of user defined types serve as examples of a large percentage of concepts and techniques that one could encounter in a mature GEF editor.

Fig 1. The shape diagram editor running under Linux

Download and unzip the latest 3.1 GEF Examples from the GEF Project Downloads into your main Eclipse directory. To create a new diagram, launch the wizard by pressing Ctrl-N. Expand the Examples folder and select Shapes Diagram. The following sections give a detailed overview of the shape diagram inner workings. Before we dive into code, let us start with a big picture tour of the main GEF ideas.

Core GEF concepts

GEF assists you in building a visual editor of your data. The data may be as simple as a thermostat with a single temperature knob, or as complex as a virtual private network with hundreds of routers, connections, and quality of service policies. To the credit of the GEF designers, they managed to create a framework that works with any data, or in GEF terminology, with any model. This is achieved by strictly following the Model-View-Controller pattern. The model is your data. To GEF, a model is any plain old Java object. The model should not know anything about either the controller or the view. The view is the visual representation of the model or one of its parts on the screen. It may be as simple as a rectangle, line or ellipse, or as complex as a nested logical circuit. Again, the view should remain ignorant about both the model and the controller. GEF uses Draw2D figures as views, although anything that implements the IFigure interface will do. The controller, known as an edit part, is the one that brings the two together. A top level controller is created when you start editing your model. If the model consists of many fragments, the top level controller informs GEF about that fact. In turn, child controllers for each fragment are created. If those again consists of subparts, the process is repeated until all objects that comprise a model have their controllers built. The other task of the controller is to create a figure representing the model. Once the model has been set on a particular controller, the GEF asks it for the congruous IFigure object. Since neither the model nor the view know about each other, it is the task of the controller to listen to changes in the model and update the visual representation of it. As a result, a common pattern in many GEF editors is a model that posts PropertyChangeEvent notifications. When an edit part receives an event notification it reacts appropriately by adjusting visual or structural representation of the model.

Another aspect of visual editing is reacting to user actions and mouse or keyboard events. The challenge here is to provide a mechanism that comes with sensible defaults, yet at the same time is flexible enough to allow those defaults to be replaced by interactions appropriate for the edited model. Take a mouse drag event as an example. If we were to assume every time a mouse drag event is detected that all selected objects are moved, we'd limit the freedom of the editor developer. It is quite likely that somebody might wish to provide zoom in or out operations on a mouse being dragged. GEF solves this issue by using tools, requests, and policies.

A tool is a stateful object that translates low level events, such as mouse pressed, mouse dragged, and so on, into high level requests, represented by a Request object. Which request is posted depends on which tool is active. For example, the connection tool, upon receiving a mouse pressed event, posts a connection start or connection end request. If it was a create tool, we'd receive a create request. GEF comes with a number of predefined tools and means of creating application specific tools. Tools may be activated programmatically or as a response to a user action. Most of the time, tools post requests to the EditPart whose figure was underneath the mouse. For example, if you click on a rectangle representing a widget, the edit part associated with it receives a selection or direct edit request. Sometimes, like the MarqueeSelectionTool does, the request is posted to all parts whose figures are contained within a given area. Regardless of how one or more edit parts are chosen as the target of requests, they do not handle requests themselves. Instead, they delegate this task to registered edit policies. Each policy is asked for a command for a given request. A policy not wishing to handle the request may return a null. The mechanism of having policies rather than an edit part respond to requests allows to keep both of them small and highly specialized. This, in turn, means easy to debug and more maintainable code. The final piece of the puzzle is commands. Rather than modifying the model directly, GEF requires that you do it with the help of commands. Each command should implement applying and undoing changes to the model or its part. This way GEF editors automatically support the undo/redo of model alterations.

A significant benefit of using GEF, in addition to being able to boast about your skills and design pattern knowledge, is the fact that it fully integrates with the Eclipse platform. Objects selected in the editor may provide properties for the standard Properties view. Eclipse wizards may be used to create and initialize models edited by GEF editors. Undo and Redo items of the Edit menu may trigger undoing or redoing of GEF editing changes. Simply put, GEF editors are first class citizens of the regular Eclipse platform, with the same level of integration as a text editor or any other workbench editor, implementing IEditorPart interface.

The Model

The first step when building a GEF editor is to create a model. In our case the model consists of four types of objects: a shape diagram, which holds shapes, two shape types, and shape connections. Before we start writing code for those classes, we prepare some basic infrastructure.

Core model classes

When creating a model use the following guidelines:

As the above outlined rules are common for all models, it is beneficial to create a hierarchy of base classes that enforces them. The ModelElement extends Java's Object class, adding three features: persistence, property change, and property source support. Simple model persistence is guaranteed by implementing the java.io.Serializable interface together with the readObject method. This solution permits one to save the editor's model in a binary format. While it may work for certain applications, it does not provide format portability. In more complex cases, one may implement saving the model in XML or similar format. Model changes are communicated using property change events. The base class allows edit parts to register and unregister as receivers of property change notifications. Those are posted by calling the firePropertyChange method. Finally, in order to aid integration with the Properties view of the workbench, the IPropertySource interface is implemented (details of which are omitted in Figure 2).

public abstract class ModelElement implements IPropertySource, Serializable { private transient PropertyChangeSupport pcsDelegate = new PropertyChangeSupport(this); public synchronized void addPropertyChangeListener(PropertyChangeListener l) { if (l == null) { throw new IllegalArgumentException(); } pcsDelegate.addPropertyChangeListener(l); } protected void firePropertyChange(String property, Object oldValue, Object newValue) { if (pcsDelegate.hasListeners(property)) { pcsDelegate.firePropertyChange(property, oldValue, newValue); } } private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { in.defaultReadObject(); pcsDelegate = new PropertyChangeSupport(this); } public synchronized void removePropertyChangeListener(PropertyChangeListener l) { if (l != null) { pcsDelegate.removePropertyChangeListener(l); } } ... } Fig 2. The base class of all model objects

Two types of objects, ellipse and rectangle shapes, share further common functionality that may be factored out into a common class. In particular, both represent objects that occupy certain locations and have a non-zero size. Both may have connections ending or originating at them. Any changes to these properties need to be communicated to all listeners. Furthermore, the location and size property are also exposed through the IPropertySource interface, allowing the user to inspect and modify them via the Properties view.

Management of connections between objects is worth a more detailed look. There is no concept of a global store of all connections. Instead, GEF requires model parts to report any connections that start or terminate in them. These must be reported as Lists of objects. The Shape class maintains two array lists of source and target connections. The source connections are those which have the given shape as the source, and target connections are those in which the given shape is recorded as the target. Two methods (, ), with package level visibility, are added that allow shapes and connections to communicate about their mutual relationship. In addition, two public methods (, ) are defined that allow classes external to the model package learn about connectivity of a shape. These are used by shape controllers, explained in the subsequent part of this article.

public abstract class Shape extends ModelElement { private Point location = new Point(0, 0); private Dimension size = new Dimension(50, 50); private List sourceConnections = new ArrayList(); private List targetConnections = new ArrayList(); public Point getLocation() { return location.getCopy(); } public void setLocation(Point newLocation) { if (newLocation == null) { throw new IllegalArgumentException(); } location.setLocation(newLocation); firePropertyChange(LOCATION_PROP, null, location); } void addConnection(Connection conn) { if (conn == null || conn.getSource() == conn.getTarget()) { throw new IllegalArgumentException(); } if (conn.getSource() == this) { sourceConnections.add(conn); firePropertyChange(SOURCE_CONNECTIONS_PROP, null, conn); } else if (conn.getTarget() == this) { targetConnections.add(conn); firePropertyChange(TARGET_CONNECTIONS_PROP, null, conn); } } void removeConnection(Connection conn) { if (conn == null) { throw new IllegalArgumentException(); } if (conn.getSource() == this) { sourceConnections.remove(conn); firePropertyChange(SOURCE_CONNECTIONS_PROP, null, conn); } else if (conn.getTarget() == this) { targetConnections.remove(conn); firePropertyChange(TARGET_CONNECTIONS_PROP, null, conn); } } public List getSourceConnections() { return new ArrayList(sourceConnections); } public List getTargetConnections() { return new ArrayList(targetConnections); } ... } Fig 3 Shape functionality

Top level model classes

With the above preparation we may start coding top level model classes. The Connection class represents a connection between two shapes. It stores the source and target of a connection. Changes in connectivity are effected by invoking disconnect or reconnect methods. Connections maintain a boolean flag indicating if they are currently connected or disconnected. The flag is used by commands to verify legitimacy of certain operations. Both source and target retain references to the original shapes allowing disconnected connections to be easily reconnected. Connections maintain one attribute, the line style. The EllipticalShape and RectangularShape classes provide an extension to the above described Shape class, with a minimum of functionality added.

The ShapeDiagram class extends the ModelElement class with the container functionality. It maintains a collection of shapes and notifies listeners about collection changes. The boolean values returned by the addChild and removeChild methods are used by commands to perform validation of their operations. Public access to all shapes in a diagram is provided for the benefit of the controller class.

public class ShapesDiagram extends ModelElement { ... private Collection shapes = new Vector(); public boolean addChild(Shape s) { if (s != null && shapes.add(s)) { firePropertyChange(CHILD_ADDED_PROP, null, s); return true; } return false; } public List getChildren() { return new Vector(shapes); } public boolean removeChild(Shape s) { if (s != null && shapes.remove(s)) { firePropertyChange(CHILD_REMOVED_PROP, null, s); return true; } return false; } } Fig 4. ShapeDiagram - a container of shapes

A note on implementation

A careful reader is certain to recognize that the model effectively created a specific implementation of a directed graph, with shapes acting as vertices, connections representing edges, and shape diagrams playing the role of the graph. The representation built here is known as adjacency list representation and is suitable for sparse graphs. With minimal effort, one could transform the model's code into a generic graph representation. The only additions to the implementation regularly presented in books on algorithms is the fact that the graph, its nodes, and its edges post events when their states change. Also nodes, unlike in mathematical graphs, rather than being zero-dimensional points, have rectangular bounds. Finally, while regular graphs act as a central global storage of edges, a diagram does not hold connections, as GEF does not require it.

It is worth noting that the solutions employed by the above presented classes are not the only ones possible. Those who developed computer representations of graphs may prefer alternative ways of storing connections or arranging communications between nodes and edges. However, such details are not important. Designers are free to choose their own more generic, faster, or otherwise enhanced model representation. The vital part is event based notification of model changes, maintenance of all, including visual attributes of the model, and support for model persistence. Depending on your experience and needs, the rest are traits which you should feel free to alter.

The Views

Due to the simplicity of the shape diagram editor, we do not have to create figures representing our model, but use predefined figures instead. A diagram is represented by the Figure class equipped with the FreeformLayout manager. This gives us the freedom to drag and drop objects at any location. Objects are represented either by the RectangleFigure or by Ellipse. Relying on predefined figures to represent parts of the model is uncustomary. Even though your view may not have any references to either the model or the controller, it must have a visual attribute for every important aspect of the model that the user may wish to inspect or change. It is thus much more common to define intricate figures with the number of visual attributes, such as color, text, nested figures, etc., matching the number of attributes of the model they represent. For a more thorough treatment about creating complex figures please see [4].

The Parts

For each independent piece of the model we must define a controller. By "independent" we mean an element that may be subject to user manipulations. A good rule of thumb to follow is that anything that may be selected or deleted should have its own edit part.

The role of edit parts is to understand the model, listen to events about its changes, and update views, correspondingly. Due to the choices made at the model level, all edit parts follow the pattern shown in Figure 5. Each part implements the PropertyChangeListener interface. When activated, it registers with the model as the receiver of the property change events. Upon deactivation, it removes itself from the list of listeners. Finally, when it receives a property change event, based on the name of the property and the new and old values it refreshes one or more visual aspects of the figure representing the model. In fact, this pattern is so common that in a larger application it would justify creating a base class factoring out this behavior.

public abstract class SpecificPart extends AbstractGraphicalEditPart implements PropertyChangeListener { public void activate() { if (!isActive()) { super.activate(); ((PropertyAwareModel) this.getModel()).addPropertyChangeListener(this); } } public void deactivate() { if (isActive()) { ((PropertyAwareModel) this.getModel()).removePropertyChangeListener(this); super.deactivate(); } } public void propertyChage(PropertyChangeEvent evt) { String prop = evt.getPropertyName(); ... } } Fig 5. Property aware part

The DiagramEditPart class

When the editor successfully loads and sets a shape diagram object on a graphical viewer, the ShapesEditPartFactory is asked to create a part controlling the diagram. It creates a new DiagramEditPart and sets the diagram as its model. The newly created part is activated, registers itself with the model, and creates a figure, with a free form layout manager that allows positioning of diagram figures based on their constraints (bounds). The DiagramEditPart reports all shapes contained in the diagram via the getModelChildren. As mentioned before, GEF repeats the process of generating parts and figures for all returned model children.

The DiagramEditPart class installs three policies. All policies should be installed in the createEditPolicies method that is declared by the AbstractEditPart class, and must be implemented by all concrete classes extending the AbstractGraphicalEditPart. Policies are delegates used by edit parts to handle requests posted by tools. In the simplest cases, policies take care of generating commands. A policy is registered with the String key, referred to as the policy's role. The key has no meaning to edit parts. However, it should have meaning to a software developer, as it allows others, specifically those who extend your controller, to disable or remove the policy by specifying its key. As far as GEF is concerned, your key could be "foobar". However, you'd better tell your fellow developers that in order to, say, set a new layout policy when the layout manager is changed, they need to install a new "foobar" policy. As this might be amusing, but not obvious, it is recommended that you use keys defined in the EditPolicy interface, whose names try to convey the role the given policy plays in an edit part.

The first policy installed using the EditPolicy.COMPONENT_ROLE key has the task of preventing the root of the model from being deleted. It overrides the createDeleteCommand method to return an unexecutable command. The second policy, installed with the LAYOUT_ROLE key, handles create and constraint change requests. The first request is posted when a new shape is dropped into a diagram. The layout policy returns a command that adds a new shape to the diagram editor and places it at the drop location. The constraint change request is posted whenever the user resizes or moves shapes already present in the diagram. The third call to the installEditPolicy removes rather than installs a policy. This prevents the root part from providing selection feedback when the user clicks on the area of the diagram corresponding to the root of the model. This call also illustrates the importance of meaningful keys used to register part's policies.

protected void createEditPolicies() { installEditPolicy(EditPolicy.COMPONENT_ROLE, new RootComponentEditPolicy()); XYLayout layout = (XYLayout) getContentPane().getLayoutManager(); installEditPolicy(EditPolicy.LAYOUT_ROLE, new ShapesXYLayoutEditPolicy(layout)); installEditPolicy(EditPolicy.SELECTION_FEEDBACK_ROLE, null); } Fig 6. Policies installed by the diagram edit part.

The diagram edit part monitors child added and child removed property events. These are posted by the ShapesDiagam class whenever new shapes are added or removed. Upon detecting either type of a property change event, the diagram edit part invokes the refreshChildren method, defined in the AbstractEditPart. This method traverses all model children and creates, removes, or re-orders edit part children appropriately.

The ShapeEditPart class

Diagram shapes are managed by the ShapeEditPart. The part itself is created by the ShapesEditPartFactory in response to DiagramEditPart returning a list of model children. Each part created by the factory is given the child model which it controls. Once the model is set, the part is asked to create a figure representing it. Depending on the type of the model, it returns either an ellipse or a rectangle.

Shape edit parts monitor four types of property change events: size, location, source, and target connections. If the shape changes size or location, the refreshVisual method is called. This method is automatically invoked by GEF the first time a figure is created. In it, the visual attributes of the figure should be adjusted based on the state of the model. Reusing the same method for model updates is another frequently encountered pattern in GEF editors. In the case of the shape editor part, the new location and size are fetched and stored with the figure representing the shape. In addition, the new bounds are passed as the constraint to the layout manager of the parent controller. When source or target connections change, the source or target connection edit parts are refreshed by a call to the methods defined in the AbstractGraphicalEditPart. Similarly to the refreshChildren method, these methods go through the list of connections and add, remove, or reposition edit parts corresponding to them.

class ShapeEditPart extends AbstractGraphicalEditPart implements PropertyChangeListener, NodeEditPart { protected List getModelSourceConnections() { return getCastedModel().getSourceConnections(); } protected List getModelTargetConnections() { return getCastedModel().getTargetConnections(); } public ConnectionAnchor getSourceConnectionAnchor(ConnectionEditPart connection) { return new ChopboxAnchor(getFigure()); } public ConnectionAnchor getSourceConnectionAnchor(Request request) { return new ChopboxAnchor(getFigure()); } public void propertyChange(PropertyChangeEvent evt) { String prop = evt.getPropertyName(); if (Shape.SIZE_PROP.equals(prop) || Shape.LOCATION_PROP.equals(prop)) { refreshVisuals(); } if (Shape.SOURCE_CONNECTIONS_PROP.equals(prop)) { refreshSourceConnections(); } if (Shape.TARGET_CONNECTIONS_PROP.equals(prop)) { refreshTargetConnections(); } } protected void refreshVisuals() { Rectangle bounds = new Rectangle(getCastedModel().getLocation(), getCastedModel().getSize()); figure.setBounds(bounds); ((GraphicalEditPart) getParent()).setLayoutConstraint(this, figure, bounds); } } Fig 7. Controller of shapes

As shapes may be connected to other shapes, the shape edit part overrides the getModelSourceConnections and the getModelTargetConnections methods. The role of these methods is to inform GEF about connections that originate or terminate at the given shape. In addition, the ShapeEditPart implements the NodeEditPart interface. By implementing it, the edit part is able to define source and target anchors, i.e., points to which connections attach. The logic circuit editor example uses this feature to indicate where a wire would attach to a logical gate. Since shapes do not have any specific connection points, we use a chop box anchor which clips the connection against the rectangular bounds of the figure. If you wish, you can return the EllipseAnchor for ellipse shapes, which returns a point on the ellipse's boundary. For more complex shapes, you should extend the AbstractConnectionAnchor class and implement the getLocation method. Notice that two types of methods are implemented: one taking a ConnectionEditPart, and one taking a Request as the parameter. The second method is invoked to provide a user with feedback while a new connection is being created. The first one is used for established connections.

Shape edit part installs two policies. The ShapeComponentEditPolicy supplies a command for removing a shape from the diagram. The second policy, installed with the GRAPHICAL_NODE_ROLE key, handles the task of creating and reattaching connections between shapes. A new connection is created in two steps by the connection creation tool. When a user clicks on a figure corresponding to an element of the model, the policy is requested to create a connection command. Returning null from this method indicates that the connection may not originate from the given element of the model. If the connection is possible, a new command should be created and stored in the request as the start command. When another figure is clicked, the policy is required to supply a connection complete command. This could be a new command built from the start command, or the start command tag supplied with the information about the terminating point of the connection.

new GraphicalNodeEditPolicy() { protected Command getConnectionCreateCommand(CreateConnectionRequest request) { Shape source = (Shape) getHost().getModel(); int style = ((Integer) request.getNewObjectType()).intValue(); ConnectionCreateCommand cmd = new ConnectionCreateCommand(source, style); request.setStartCommand(cmd); return cmd; } protected Command getConnectionCompleteCommand(CreateConnectionRequest request) { ConnectionCreateCommand cmd = (ConnectionCreateCommand) request.getStartCommand(); cmd.setTarget((Shape) getHost().getModel()); return cmd; } ... } Fig 8. Graphical node edit policy

The other task of the graphical node edit policy is to provide connection reattachment commands. Connection reattachment may change the source or the target of the connection. The same rules apply to these commands as to the connection creation command. In particular, if a given connection should not be reattached, the policy must return null. It is also possible for the policy to return a command that refuses to be executed, by returning false from the canExecute method. Due to space limitation, details of these commands are left out and the reader is referred to the source code.

The ConnectionEditPart class

As connections are user editable parts of the model, they must have their own controller. It is implemented by the ConnectionEditPart class, which extends the AbstractConnectionEditPart class. Similar to other controllers, it implements the PropertyChangeListener interface and registers the part for the events with the model on activation. The connection part returns a polyline decorated with an arrow as the figure. It installs two edit policies. The first one, the ConnectionComponentPolicy, supplies a delete command needed by the action associated with the Delete menu item. The second one is of greater interest. It equips a selected connection with handles, placed at the start and the end. Without this policy, reattaching connections is impossible, as the GEF has no handles to grab onto when the end of the connection is being dragged. The authors of the GEF recommend that all ConnectionEditParts should have this policy, even if their ends are not draggable. At minimum this policy provides a visual selection feedback. The propertyChange method watches for changes in the line style property and adjusts the polyline figure appropriately.

class ConnectionEditPart extends AbstractConnectionEditPart implements PropertyChangeListener { protected IFigure createFigure() { PolylineConnection connection = (PolylineConnection) super.createFigure(); connection.setTargetDecoration(new PolygonDecoration()); connection.setLineStyle(getCastedModel().getLineStyle()); return connection; } protected void createEditPolicies() { installEditPolicy(EditPolicy.CONNECTION_ROLE, new ConnectionEditPolicy() { protected Command getDeleteCommand(GroupRequest request) { return new ConnectionDeleteCommand(getCastedModel()); } }); installEditPolicy(EditPolicy.CONNECTION_ENDPOINTS_ROLE, new ConnectionEndpointEditPolicy()); } public void propertyChange(PropertyChangeEvent event) { String property = event.getPropertyName(); if (Connection.LINESTYLE_PROP.equals(property)) { ((PolylineConnection) getFigure()). setLineStyle(getCastedModel().getLineStyle()); } } ... } Fig 9. Controller of connections

Shape Editor

The shape editor extends the GraphicalEditorWithFlyoutPalette class. This class is a specialized form of a graphical editor, a type of an editor part, equipped with a palette hosting tool entries. The extending class must implement two methods, getPaletteRoot and getPalettePreferences. The first method must return a palette root populated with tool entries. Tool entries are specialized types of palette entries capable of installing tools on the edit domain of the editor. They may be hosted in palette drawers, which provide convenient way of grouping them. It is recommended that one tool entry is set as the default entry of the palette root. A typical solution is to use an instance of the SelectionToolEntry class in that role. Palette preferences, returned by the second method, specify whether the palette is visible or collapsed, the location where it is docked, and the palette width. A commonly found solution is to save them to and restore them from the plug-in's preference store.

The already mentioned edit domain plays the role of a central controller. It holds a palette of tools, loads the default tool, maintains the active tool to which it forwards mouse and key events, and deals with the command stack. GEF provides the default implementation, the DefaultEditDomain, which you should set on your editor in the constructor.

Part of the job that a graphical editor must perform is to create and initialize a graphical viewer. A graphical viewer is a specialized EditPartViewer capable of performing hit testing. Again, we may rely on the default viewer supplied by the GraphicalEditor class. There are, however, a few things that need to be done. In the configureGraphicalViewer method set a factory of edit parts. The factory must implement the sole method of the EditPartFactory interface, createEditPart(EditPart, Object). The first argument is the edit part that returned the second argument, a (part of your) model, through the getModelChildren method. Other things to do here may include setting up a key handler, context menus, etc.

protected void configureGraphicalViewer() { super.configureGraphicalViewer(); GraphicalViewer viewer = getGraphicalViewer(); viewer.setRootEditPart(new ScalableRootEditPart()); viewer.setEditPartFactory(new ShapesEditPartFactory()); viewer.setKeyHandler( new GraphicalViewerKeyHandler(viewer).setParent(getCommonKeyHandler())); ContextMenuProvider cmProvider = new ShapesEditorContextMenuProvider(viewer, getActionRegistry()); viewer.setContextMenu(cmProvider); getSite().registerContextMenu(cmProvider, viewer); } protected void initializeGraphicalViewer() { super.initializeGraphicalViewer(); GraphicalViewer graphicalViewer = getGraphicalViewer(); graphicalViewer.setContents(getModel()); graphicalViewer.addDropTargetListener(createTransferDropTargetListener()); } Fig 10. Configuring and initializing a graphic viewer

Once the factory is set, you should set the contents on the graphical viewer. The contents naturally should be the object restored from the IEditorInput passed to the editor in the setInput method. The shape example also adds a drop target listener to the graphical viewer. This allows one to use the drag and drop gesture rather than select and click when adding new shapes to the diagram. The drop target listener uses a subclassed TemplateTransferDropTargetListener that uses a CreateRequest to fetch a command for adding an object to the model owned by the edit part above which the drag and drop gesture was finalized.

In addition to the described tasks, the editor takes care of reporting the dirty flag by monitoring a command stack. This is a preferred solution, as this keeps the flag in synch with any undo or redo actions that the user may perform. Notice that the command stack has the save location marked in both doSave and doSaveAs methods. Other details of the editor, such as actual saving and restoring of the model, are not discussed here as they tend to be very application specific. The editor's functionality that deals with exposing editor content to other views, connecting menu items to editor actions, and other workbench cooperation techniques is described next.

Integrating with the workbench

The editor, as presented so far, would be fully operational. However, it would not integrate well with the workbench. For example, the Edit menu actions, such as Delete, Undo, and Redo could not be used. Other views could not show alternative presentations of the editor content. In other words, the editor would not get the benefits of being part of the Eclipse workbench. The task of transforming an isolated editor into a proper participant of the workbench is explained in the following three sections.

Editor Actions

The ShapesEditor class creates a number of default actions in the createActions method invoked during editor initialization. These are undo, redo, select all, delete, save, and print actions. In order to connect standard menu items to them, you should define an action bar contributor and list it, in the plugin.xml file, as the editor contributor. In the action bar contributor you need to implement two methods. The first one, the buildActions method, should create retargetable actions for undo, redo, and delete. If you wish to enable keyboard selection of all widgets, you need to add a global action key for the selected action in the second method, declareGlobalActionKeys.

public class ShapesEditorActionBarContributor extends ActionBarContributor { protected void buildActions() { this.addRetargetAction(new UndoRetargetAction()); this.addRetargetAction(new RedoRetargetAction()); this.addRetargetAction(new DeleteRetargetAction()); } public void contributeToToolBar(IToolBarManager toolBarManager) { super.contributeToToolBar(toolBarManager); toolBarManager.add(getAction(ActionFactory.UNDO.getId())); toolBarManager.add(getAction(ActionFactory.REDO.getId())); } protected void declareGlobalActionKeys() { this.addGlobalActionKey(ActionFactory.SELECT_ALL.getId()); } } Fig 11. Connecting menu actions

It may be illustrative to trace what happens when the user selects the Delete item in the Edit menu (see Figure 12). The delete action, which is added to the action registry by the parent class of the ShapesEditor class, traces the current selection. When the delete action is executed, it checks if any of the currently selected objects are instances of the EditPart class. For each such object it requests a command from the edit part. In turn, each edit part checks if any of the edit policies created on it understand and are willing to handle the delete request. For shapes, the ShapeComponentEditPolicy claims it can handle the delete request, and when asked for a command it returns a ShapeDeleteCommand instance. The action executes the command, which removes the shape from the diagram. The diagram fires a property change event that is handled by the DiagramEditPart and ultimately leads to a rectangle or ellipse representing the deleted shape to be removed from the display.

Fig 12. Delete action call sequence

Exposing properties

Every graphical editor is a source of selection events. You can test this by creating a view that registers with the workbench site's page as a selection listener. Every time you select an object in your graphical editor, your view receives a notification in the selectionChanged method. One of Eclipse's standard views, Properties view, listens to selection events, and for every selection checks if its objects implement the IPropertySource interface. If so, it uses the methods of the interface to interrogate the selected object or objects about their properties and displays them in a tabular format.

Thanks to the above described infrastructure, exposing properties of objects edited in graphical editor is a matter of implementing methods of the IPropertySource interface. By inspecting the Shape class you can view how position and size of objects are made available to the Properties view.

Providing an Outline

The Outline view is used to provide an alternative and often more succinct view of edited data. In Java editors it is used to show imports, variables, and methods of the edited class, without going into code details. Graphical editors can also benefit from such a high level view. The shape diagram editor, similarly to the logic circuit editor, exposes the edited contents in the form of a tree (see Figure 1). The database schema editor [7] provides a view of the entire editor window with a thumb for panning.

In order to expose edited content to the Outline view, you need to override the getAdapter method and return an outline implementation when the adapter class is the IContentOutlinePage interface. The easiest way to implement an outline is to extend the ContentOutlinePage class by supplying it with an appropriate configured EditPartViewer.

public Object getAdapter(Class type) { // returns the content outline page for this editor if (type == IContentOutlinePage.class) { if (outlinePage == null) { outlinePage = new ShapesEditorOutlinePage(this, new TreeViewer()); } return outlinePage; } return super.getAdapter(type); } Fig 13. Providing an overview

In the case of the shape diagram editor, the edit part view is implemented by a tree viewer. You should supply it with the same edit domain as your main editor. A tree viewer, just like any other EditPartViewer, requires a method for creating child edit parts. The editor uses the same mechanism as that employed with the DiagramEditPart, by setting an edit part factory on it. In addition, the selection of the overview and the main editor window is synchronized using a selection synchronizer, a GEF utility class that reconciles the selection state of two edit parts. The ShapesTreeEditPartFactory returns either a ShapeTreeEditPart or a DiagramTreeEditPart instance, depending on the model type. By inspecting those classes, the reader should have no difficulty recognizing already familiar patterns. Both edit parts implement the PropertyChangeListener interface and react to property changes by adjusting visual representation of the model. Both install edit policies to control types of interactions exposed through them.

Design patterns used by GEF

GEF attains its flexibility through an extensive use of design patterns. Provided here is a brief summary of those most commonly encountered. For a more detailed treatment on patterns please see [2].

The MVC pattern is used by GEF to decouple user interface, behavior, and presentation. The model is represented by any Java Object. The view must implement the IFigure interface. The controller is a type of an EditPart.
Commands encapsulate model changes, therefore providing support for undoable operations.
Chain of Responsibility
Decouples senders of requests (tools) from receivers by giving more than one object a chance to handle the request. In the case of GEF, multiple edit policies may respond to a request with Commands which then are chained together.
Allows editor to alter its behavior when its internal state changes. With GEF editors, this change is implemented by switching tools. For example, a marquee tool causes the editor to respond differently to a mouse down event than when a create tool is active.
Abstract Factory
Provides an interface for creating families of related or dependent objects. This pattern is used when creating edit parts controlling given model parts.
Factory Method
Defines a method for creating an object, but lets subclasses decide which class to instantiate. While not explicitly discussed, this is an alternative method for creating an edit part. The method createChild allows you to explicitly create child edit parts without using a factory.


I tried giving a detailed description of most aspects of a very simple graphical editor. Hopefully there is enough information provided to allow anybody patient enough to read this lengthy essay to inspect larger examples, such as the logic circuit editor. By immediately understanding roles of classes such as CircuitEditPart, AndGateFigure, and a few others that directly correspond to classes present in the simple shape editor, you may focus your attention on more complex aspects of larger examples. There exists a plethora of subjects and techniques in GEF whose surface I have not even scratched. However, they should be studied only after the base is well understood. After all, what is the purpose of trying to design a drag feedback, if it takes you a few hours to enable the Select All menu item?


I would like to thank Randy Hudson for his comments that helped improve the structure and accuracy of this article. My thanks also go to Jill Sueoka for tirelessly reviewing numerous versions that I managed to produce.


<table> <tr> <td valign="top">[1]</td> <td> Eric Bordeau, Using Native Drag and Drop with GEF, Eclipse Corner Article, August 2003 </td> </tr> <tr> <td valign="top">[2]</td> <td> Erich Gamma, Richard Helm, Ralph Johnson and John Vlissides, Design Patterns: Elements of Reusable Object-Oriented Software, Addison Wesley, 1995, ISBN 0-201-63361-2</td> </tr> <tr> <td valign="top">[3]</td> <td> Randy Hudson, Create an Eclipse-based application using the Graphical Editing Framework, IBM developerWorks, July 2003 </td> </tr> <tr> <td valign="top">[4]</td> <td> Daniel Lee, Display a UML Diagram using Draw2D Diagram, Eclipse Corner Article, August 2003 </td> </tr> <tr> <td valign="top">[5]</td> <td> Xavier Mehaut et al., Synthetic GEF description, June 2004 </td> </tr> <tr> <td valign="top">[6]</td> <td> William Moore, David Dean, Anna Gerber, Gunnar Wagenknecht and Philippe Vanderheyden, Eclipse Development using the Graphical Editing Framework and the Eclipse Modeling Framework, IBM RedBooks, 2004, ISBN 0738453161</td> </tr> <tr> <td valign="top">[7]</td> <td> Phil Zoio, Building a Database Schema Diagram Editor with GEF, Eclipse Corner Article, September 2004 </td> </tr> </table>

Java and all Java-based trademarks and logos are trademarks or registered trademarks of Sun Microsystems, Inc. in the United States, other countries, or both.

前一篇: Folding in Eclipse Text Editors
后一篇: Modeling Rule-Based Systems with EMF