The control defines the mapping from user events to methods of the graph- and selection model and the view. It is implemented in a platform-dependent way in the UI-delegate, and basically deals with in-place editing, cell handling, and updating the display. As the only object that is exchanged when the look and feel changes, the UI-delegate also specifies the look and feel specific part of the rendering.
The GraphUI
abstract class and its default implementation, the BasicGraphUI
constitute JGraph's UI-delegate. Aside from the event listeners to track user input, the UI-delegate is also used to paint the graph, and update the display when the graph or the selection changes. When a graph is painted, the UI-delegate loops through the views and paints them by painting their respective renderers.
The GraphUI
abstract class defines the methods for an object that may be used as a UI-delegate for JGraph, whereas the BasicGraphUI
provides default implementations for these methods. The BasicGraphUI
in turn may be subclassed to implement the UI for a specific look and feel, but the default implementation already contains some look and feel specific coloring.
The BasicGraphUI
class maintains a reference to a RootHandle
, which is used to react to mouse events. The mouse listener, which calls this handler, is an inner-class of the UI-delegate. It defines the basic behavior of JGraph with regard to mouse events, and implements the two main functionalities: selection and editing. Selection includes single-cell and marquee selection. Editing can either be based on the CellEditor
interface, for in-place editing, or on the CellHandle
interface, for cell handling.
The BasicGraphUI
creates the root handle, whereas the cell handles are created by the cell views. The root handle is responsible to move the selection, whereas its children are used to change the shape of the cells. A set of default keyboard bindings is supported by the UI-delegate, and listed in Table 1. (The number of clicks that trigger in-place editing can be changed using the setEditClickCount
method of JGraph.)
Renderers do not paint cells; they paint objects that implement the CellView
interface. For each cell in the model, there exists exactly one cell view in each graph view, which has its own internal representation of the graph model. The renderers are instantiated and referenced by the cell views.
Renderers are based on the idea of the TreeCellRenderer
class [bib-View] from Swing, and on the Flyweight design pattern (see [bib-GOF], page 195 ff). The basic idea behind this pattern is to "use sharing to support large numbers of fine-grained objects efficiently." [bib-GOF]
Because having a separate component for each CellView
-instance would be prohibitively expensive, the component is shared among all cell views of a certain class. A cell view therefore only stores the state of the component (such as the color, size etc.), whereas the renderer holds the component's overhead, and the painting code (for example a JLabel
instance).
The CellView
s are painted by configuring the renderer, and painting the latter to a CellRendererPane
[bib-Java1.4], which may be used to paint components without the need to add them, as in the case of a container. Configuring a renderer means to fetch the state from the cell view, and apply it to the renderer. For example in the case of a vertex, which uses a JLabel
instance as its renderer, the cell's label is retrieved using graph.convertValueToString, and set using setText
on the renderer, together with other supported attributes that make up the state of a vertex.
The renderers in JGraph are used in analogy to the renderer in JTree, just that JGraph provides more than one renderer, namely one for each type of cell. Thus, JGraph provides a renderer for vertices, one for edges, and one for ports. For each subtype of the CellView
interface, overriding the getRenderer
method may associate a new renderer. The renderer should be static to allow it to be shared among multiple instances of a class.
The renderer itself is typically an instance of the JComponent
class, with an overridden paint
method that paints the cell, based on the attributes of the passed-in cell view. The renderer also implements the CellViewRenderer
interface, which provides the getRendererComponent
method to configure the renderer.
This interface provides two methods: one to configure and return the renderer, the other to check if an attribute is supported. The first method's arguments are used to specify the cell view and its state, that is, if it is selected, if it has focus, and if it is to be painted as a preview or in highlighted state.
The preview argument is true if the renderer is used in the context of live-preview, which requires faster painting. In this case, the renderer may decide not to paint the cell in full quality, based on the cost of this painting. The arguments passed to this method can not be deduced from the cell view; they are determined by the UI-delegate, the selection-model or from the context in which the renderer is used.
The three default implementations of the CellViewRenderer
interface are the VertexRenderer,
EdgeRenderer
and PortRenderer
classes. The VertexRenderer
is an extension of JLabel,
which offers the code to paint the label, the icon, the border and background of a vertex. The paint
method is only overridden to paint the selection border.
The other renderers are subclasses of the JComponent
class, because they perform special rendering, which is not offered by any of the JComponent
extensions. The EdgeRenderer
uses the Graphics2D
class and the Shape
interface to paint the label, edge, and decorations. The PortRenderer
provides the code to paint a small cross, or a highlighted port in the case where the highlight argument is true.
The preview flag is ignored in the context of PortRenderer
s because in live-previews, ports are never painted, and therefore, the flag is never true when used in a PortRenderer
.
In-place editing is accomplished through a set of methods that the UI-delegate provides. Internally, the object that is used as an editor is retrieved from the cell view, just like the renderer is. In contrast to the renderer, however, there is only one GraphCellEditor
for all cells, even if the design allows different editors to be specified for each view type, even for each view instance.
As in the case of the CellViewRenderer
interface, the GraphCellEditor
interface provides a method that is used to return a configured editor for the specified cell. The argument to this method is not a CellView
, as in the case of CellViewRenderer
; it is a cell instead, which is of type Object. This underlines the fact that the label is typically stored in the model, not in the view.
The editor is placed inside the graph by the UI-delegate, which is also responsible to remove it from there, and dispatch the change to the model. Thus, for customized editing, it might be necessary to provide a custom UI-delegate, overriding the protected startEditing and completeEditing
methods of the BasicGraphUI
class.
Cell handles are a new concept that is not used elsewhere in Swing. This is because in Swing, the only method to edit cells is by means of in-place editing. It is not possible to change the bounds of a cell, or to move the cell to an arbitrary location. Rather, the UI-delegate and the state of the component determine the location of a cell. In JGraph instead, the position and size are stored in the attributes, and do not depend on the state of the graph. Therefore, a way must be provided to change the attributes in a transaction-oriented way.
Handles are based on the Composite pattern, where a root object provides access to children based on a common interface, namely the CellHandle
interface. The UI-delegate creates a RootHandle
upon a selection change. The root handle, in turn, uses the CellView
's getHandle
method to create its child handles.
While the root handle is active, the UI-delegate's internal mouse handler messages its methods, and the root handle in turn delegates control to the correct subhandle, or absorbs the events in order to move the selection.
During a change to the graph, the new attributes are displayed by use of temporary cell views, which have previously been created using the GraphContext
object. This is referred to as the live-preview, meaning that the graph is overlaid with the change, so that the user can see what the graph will look like when the mouse is released. (The change may be cancelled by pressing the Escape key.)
If the model is an attribute store, then the change is executed on the model. Otherwise, the change is executed on the view. In both cases, the applyMap
method is used, which is provided by the GraphConstants
class.
The CellHandle
interface is very similar to a MouseAdapter
, providing all the methods that are used to handle mouse events. In addition, the interface defines two methods to paint the handle, namely the paint
method, which is used to draw the static part (the actual handles), and the overlay
method, which is used to draw the dynamic part (the live-preview), using fast XOR'ed-painting.
The default implementations of the CellHandle
interface are the SizeHandle,
EdgeHandle,
and RootHandle
classes. The root handle is responsible to move cells, the size handle is used to resize cells, and the edge handle allows to connect and disconnect edges, and to add, modify or remove individual points.
The UI-delegate provides an implementation of the TransferHandler
class, which in turn is responsible to create the Transferablebased on the current selection.
The transferable is represented by the GraphTransferable
class, which in turn has a reference to the ConnectionSet
, the cells and their corresponding view's attributes. The ConnectionSet
is created using the static factory method that was outlined before. The data may be returned as serialized objects, or as plain text or HTML. In the latter two cases the label of the selected cell is returned, or an empty string, if the transferable contains more than one cell.
Marquee selection is the ability to select a rectangular region by use of the mouse, which is not provided by Swing. The BasicMarqueeHandler
class is used to implement this type of selection. From an architectural point of view, the marquee handler is analogous to the transfer handler, because it is a "high-level" listener that is called by low-level listeners, such as the mouse listener, which is installed in the UI-delegate.
With regard to its methods, the marquee handler is more similar to the cell handle, because the marquee handler deals with mouse event, and allows additional painting and overlaying of the marquee. (The marquee is a rectangle that constitutes the to-be selected region.)
In JGraph, the graph model, selection model and graph view may dispatch events. The events dispatched by the model may be categorized into
Change notification
History support
The GraphModelListener
interface is used to update the view and repaint the graph, and the UndoableEditListener
interface to update the history. These listeners may be registered or removed using the respective methods on the model.
The selection model dispatches GraphSelectionEvent
s to the GraphSelectionListeners
that have been registered with it, for example to update the display with the current selection. The view's event model is based on the Observer and Observable class to repaint the graph, and also provides undo-support, which uses the graph model's implementation.
An additional notification mechanism between the model and the view enables a single model to have multiple views. Note that the view does not itself implement the GraphModelListener
interface, it is messaged by the model listener that is part of the UI-delegate, passing along the change information as an argument.
Each change is either executed on the model, or on a specific view, or it is a combination of both. No changes exist that affect multiple views at a time, except implicitly, through a change of the model. This leads to a separate notification between the local view and its UI to be repainted. The GraphLayoutCache
class implements the Observable
interface, and the BasicGraphUIprovides
an Observerinstance, which may be registered with the graph view, and is in charge of repainting the graph upon a view-only change.
Upon insertion, removal or modification of cells in the model, the model constructs an object that executes and describes the change. Because the execution and the description of such a change are closely related, these are stored in the same object. (Upon a change, the factory methods of the ParentMap
and ConnectionSet
classes are used to construct the respective objects that describe the change.)
The object is then sent to the model listeners such that they may repaint the affected region of the graph, and remove, add or update the cell views for the changed cells. The listeners might also want to ensure other properties, like for auto sizing, in which case the preferred size is recomputed when a cell has changed.
In contrast to the TreeModelListener
interface, which provides three methods to be messaged, namely one for insertion, one for removal, and one if cells are changed, the GraphModelListener
interface only provides one method to be informed on any change. (The latest release of the TreeModelListener
even has an additional method, called treeStructureChanged
.)
This has a subtle consequence when it comes to composite changes, which can insert, remove and change cells at the same time. In JTree listeners, these changes are handled by different parts of the handler code, resulting in up to four separate updates for one composite change, whereas in JGraph, the whole change is handled in one method, resulting in one single update, which is more efficient, even if such changes are rare.
Model listeners are notified using objects that implement the GraphModelEvent
interface, which in turn returns an object that implements the GraphChange
interface. The GraphChange
interface is used to describe the change, that is, it returns the cells that were added, removed, or changed. An inner class of the model, which also contains the code to execute and undo the change, implements the GraphChange
interface.
Undo-support, that is, the storage of the changes that were executed so far, is typically implemented on the application level. This means, JGraph itself does not provide a running history, it only provides the classes and methods to support it on the application level. This is because the history requires memory space, depending on how many steps it stores (which is an application parameter). Also, history cannot always be implemented, and is not always desirable.
The GraphChange
object is sent to the UndoableEditListener
s that have been registered with the model. The object therefore implements the GraphChange
interface and the UndoableEdit
interface. The latter is used to implement undo-support, as it provides an undo
and a redo
method. (The code to execute, undo and redo the change is stored within the object, and travels along to the listeners.)
Aside from the model, the graph view also uses the code that the model provides to notify its undo listeners of an undoable change. This can be done because each view typically has a reference to the model, whereas the model does not have references to its views. (The GraphModel
interface allows relaying UndoableEdit
s by use of the fourth argument to the edit
method.)
The GraphLayoutCache
class uses the model's undo-support to pass the UndoableEdit
s that it creates to the UndoableEditListener
s that have been registered with the model. Again, the objects that travel to the listeners contain the code to execute the change on the view, and also the code to undo and redo the given change.
This has the advantage that the GraphUndoManager
must only be attached to the model, instead of the model and each view.
Separate geometries, which are stored independently of the model, lead to changes that are possibly only visible in one view (view-only), not affecting the other views, or the model. The other views are unaware of the change, and if one of them calls undo, this has to be taken into account.
An extension of Swing's UndoManager
in the form of GraphUndoManager
is available to undo or redo such changes in the context of multiple views. GraphUndoManager
overrides methods of Swing's UndoManager
with an additional argument, which allows specifying the calling view as a context for the undo/redo operation.
This context is used to determine the last or next relevant transaction with respect to the calling view. Relevant in this context means visible, that is, all transactions that are not visible in the calling view are undone or redone implicitly, until the next or last visible transaction is found for the context.
As an example consider the following situation: Two views share the same model, which is not an attribute store. This means, each view can change independently, and if the model changes, both views are updated accordingly. The model notifies its views if cells are added or removed, or if the connectivity of the graph is modified, meaning that either the source or target port of one or more edges have changed. All other cases are view-only transactions, as for example if cells are moved, resized, or if points are added, modified or removed for an edge. All views but the source view are unaware of such view-only transactions, because such transactions are only visible in the source view.
In the above figure, the state of the command history is shown after a cell insertion into the model, move by the second view, and subsequent move by the first view. After insertion of the cell into the model, the cell's position is the same in all views, namely the position that was passed to the insertcall. The arrows illustrate the edits to be undone by the GraphUndoManager
for the respective views. In the case of view 3, which only sees the insert, all edits are undone.
As mentioned above, even if there are possibly many sources which notify the GraphUndoManager
, the implementation of this undo-support exists only once, namely in the graph's model. Thus, the GraphUndoManager
must only be added to one global entry point, which is the GraphModel
object.