The implementation of JGraph
is based on the design of the JTree
[bib-JTree] class, which is Swing's component for displaying trees. Rather than explaining JGraph
from scratch, this description focuses on the differences between the two classes. Swing features such as serialization, datatransfer, and the Swing MVC pattern are explained in the appendix.
JGraph
is a design extension of JTree
. In the following, these design changes are briefly outlined, pointing to the classes and methods that were adapted or introduced. The design modifications are grouped into:
Differences between trees and graphs
JGraph requirements (features)
The following important differences between trees and graphs lay the foundation of the JGraph component:
In a tree, the position and visibility of a cell depends on the expansion state. In a graph, instead, the position and size of a cell is user-defined, possibly overlapping other cells. Consequently, a way to enumerate the cells that intersect the same position, and to change the order in which they are returned is provided.
A tree consists of nodes, whereas a graph possibly consists of multiple cell types. Consequently, JGraph provides a view for each cell, which specifies the renderer, editor and cell handle. Additionally, a graph view is provided, which has its own internal representation of the graph. The idea of views has been adopted from Swing's text components [bib-View].
The mathematical definition of a graph does not include the geometric pattern or layout. Consequently this pattern is not stored in the model, it is stored in the view. The view provides a notification mechanism, and undo-support, which allows changing the geometric pattern independently of the model, and of other views. Since Swing's undo manager is not suitable for this setup, JGraph provides an extension of this Swing's default undo mechanism in the form of the GraphUndoManager
class.
The changes to meet the requirements, or features, may be grouped into:
JTree's implementation of pluggable look and feel support and serialization is used without changes.
The existing implementation of in-place editing and rendering was modified to work with views, and history.
JGraph's marquee selection and stepping-into groups extend JTree
's selection model.
JGraph is enhanced with datatransfer, attributes and history, which are Swing standards not used in JTree
. (A special history must be used in the context of separate geometric patterns.)
The layering, grouping, handles, cloning, zoom, ports and grid are new features, which are standards-compliant with respect to architecture, and coding conventions.
The pluggable look and feel and serialization are used without modifications. As in the case of JTree
, the UI-delegate implements the current look and feel, and serialization is based on the Serializable
interface, and XMLEncoder
and XMLDecoder
classes for long-term serialization.
The multiple cell types property affects the rendering and the in-place editing of JGraph. In contrast to JTree
, where the renderer and editor for the single node type is referenced from the tree object, the editors and renderers in JGraph are referenced from the cell view, to avoid a look-up of the renderer via the cell's type. The renderer is statically referenced so it can be shared among all instances of a class. Additionally, the cell view specifies a handle that allows extended editing. The idea of handles closely follows the design used for in-place editing.
Like the JTree
selection model, JGraph's selection model allows single and multiple cell selection. JGraph's selection model additionally allows to step-into groups. Marquee selection is also provided using a marquee handler, which is based on the design of Swing's transfer handler.
JGraph allows transferring the cells of a model, together with a description of their group and graph structure, and their geometric pattern either by using drag-and-drop, or via the clipboard.
Based on an idea of Swing's text components, JGraph provides maps to describe and change the attributes of a cell. These maps encapsulate the state of the cell, and may be accessed in a type-safe way using the GraphConstants
class.
JGraph provides command history, or history, which is the ability to undo or redo changes. The design follows the design of Swing's text components; however, a special undo manager must be used in the context of separate geometric patterns.
There are two groups of features in the implementation chapter:
Features that only affect the JGraph
class
Features that require new classes
The first group consists of the cloning, zoom, and grid, which are implemented by methods in the JGraph
class. The rest of the framework does not offer classes or methods to implement these features, however, it is feature-aware, which means it relies on the respective methods of the JGraph
class.
The second group consists of the layering, handles, grouping, and ports, which are not used elsewhere in Swing, and require new classes and methods. Special care has been taken to base these features on existing Swing functionalities, and make them analogous with regard to design and implementation. For example, the handles feature closely follows the design and implementation of Swing's in-place editing, so that it is easy for the programmer to adopt this new feature based on his or her understanding of in-place editing.
Because these features are new, some of them are briefly defined below:
Since cells may overlap, the order in which they are returned is significant. This order is referred to as Layering, and may be changed using the toBack
and toFront
method of the GraphLayoutCache
object. The layering is part of the view, and is explained in the view part of this document.
Handles are, like editors, objects that are used to change the appearance of a cell. In contrast to in-place editing, which uses a text component to change the value of a cell, handles use other means to provide the user with a visual feedback of how the graph will look after the successful execution of the change (live-preview). Handles and in-place editing are explained in the control part, because the UI-delegate provides this functionality.
Ports and grouping are related because ports are implemented on top of the group structure in the graph model. The grouping is therefore explained in the model part of this document.
JGraph
extends JComponent
, and has a reference to its GraphUI
. JGraph
has a reference to a GraphModel
and a GraphLayoutCache
, and instantiates BasicGraphUI
, which extends GraphUI
, which in turn extends ComponentUI
.
The basic structure of the component, namely the Swing MVC architecture, is inherited from JTree
. However, JGraph has an additional reference to a graph view, which is not typically used in Swing MVC. The graph view is analogous to the root view in Swing's text components, but it is not referenced by the UI-delegate. Instead, it is referenced by the JGraph object such that it preserves the state when the look-and-feel is changed. (The appendix provides an in-depth discussion of MVC, Swing MVC, and how it is applied to JTree
and JGraph.)
When working with attributes (see Section 2.5, “Attributes”), the startup-sequence is significant. The fact that the default model does not store attributes must be taken into account when inserting cells, because the attributes of such cells are passed to the attached views. If no views are attached, the attributes are ignored!
In the case where a view is added later, the view uses default values for the cell's positions and sizes, resulting in the fact that each cell is located at the same point, and has the same size.
Therefore, when creating a graph with a custom model, first the JGraph
instance should be created, using the model as an argument, and then, cells should be inserted into the model (not vice versa). By constructing the JGraph
instance, a view is automatically registered with the model.
The same holds for setting the model on a JGraph
object, in which case the view is notified, and holds a reference to the new model. Anyway, because the model does not store the attributes, the view will use default values as in the case where it is registered with the model after an insert call.
The above does not hold if the model's isAttributeStore
returns true, in which case all attributes are stored in the model instead of the view, making the timing issues irrelevant.
JGraph
's attributes are only conceptually based on those of Swing, some dynamic aspects, and the class to access these attributes are different from Swing, and must therefore be explained.
Attributes are implemented using maps, from keys to values, and may be accessed by use of the GraphConstants
class.
The GraphConstants
class is used to create maps of attributes, and to access the values in these maps in a type-safe way. Aside from the creation of, and the access to maps, the class also provides a method to clone maps, and a method to apply a change of more than one value on a target map.
The applyMap
method combines common entries from the target map and the change map by overriding the target values. Entries that are only present in the change map are inserted into the target map, and entries that are only present in the target map are left unchanged.
To remove entries from the target map, the setRemoveAttributes
method is used, providing the keys that should be removed as an argument. The keys are stored as an entry in the change map, and handled by the applyMap
method. If the change map replaces the target map completely, then the setRemoveAll
method must be used on the change map to indicate that all keys of the target map should be removed.
Attributes may be used in the model and in the view. In both cases, the attribute maps are created and accessed by use of the GraphConstants
class. The relation between a cell's attributes and its corresponding view's attributes is as follows:
A cell's attributes have precedence over its view's attributes, such that the cell can override the view's values for a specific key with its own value. This means, if a view specifies a value for a key that is also specified by the cell, then the cell's value is used instead of the view's value. (Two attributes are equal if the equals
method on their respective keys returns true.)
In other words, the blue attributes have precedence over the yellow ones, and if a blue attribute is not present in the yellow map, then it will be inserted. Yellow entries that do not exist as blue entries are left unchanged. (Since this mechanism is based on the applyMap
method, the behavior is exactly the same.)
The GraphConstants
class does not distinguish between the attributes for cells and views, because they are based on the same underlying structures. However, in contrast to cells, which accept all attributes, the view performs an additional test on each key. The view's renderer is used to determine if the key is supported, and if not, the entry is removed from the corresponding view's attribute map in order to reduce redundancy. (Both setAttributes
methods, for cells and views, are based on the applyMap
method.)
In JGraph's default implementation, the UI changes the view's attributes upon interactive changes. By overriding the model's isAttributeStore
method, the model can gain control. If the method returns true, then the cell's attributes will be changed instead of the view's, resulting in an immediate update of all attached views. In the default case, only the local view is updated (unless the change constitutes a change to the model).
This is because all views are updated upon a change of the model by the model's notification mechanism, and the cell's attributes are used in all views. An exception is the value attribute, which is in sync with a cell's user object. The value attribute is stored in the cell regardless of the model's isAttributeStore
method.
The setUserObject
method of the DefaultMutableTreeNode
class is overridden by the DefaultGraphCell
class such that the passed-in user object is stored under the value-key in the cell's attribute map. Vice versa, the setAttributes
method overwrites the previous user object if a new object for the value-key is specified. Thus, the value attribute points to the user object and vice versa.
The value attribute is used in the context of history and in-place editing. By introducing the value attribute, the complete state of the cell may be represented by its attributes; the user object does not require special handling. A change to the state (and equally to the user object) may be undone using the applyMap
method, providing the previous and current states as arguments.
By default, in-place editing replaces the user object with the new String, which is not always desirable. Therefore, the user object may implement the ValueChangeHandler
interface, which changes this default behavior.
The ValueChangeHandler
interface, which is an inner interface of the DefaultGraphCell
class, may be used to prevent the user object from being overwritten upon in-place editing. If a user object implements this interface, then the DefaultGraphCell
informs the user object of a change, the latter of which is responsible for storing the new, and returning the old value. The user object reflects the change through its toString
method. (JGraph's convertValuetoString
method is used to convert a cell to a String.)
Should new properties be implemented as attributes or as instance fields? In the first case, the cloning of the cell and the undo mechanism are already in place, but the attribute must be accessed through a hash table. In the second case, the cloning of the cell requires special handling, but the variable may be accessed as an instance field, which might be required for inheritance. The combination of the two leads to an increase of redundancy, and complexity. (An example is the value attribute from above.)
Basically, attributes should only be used for rendering, even if there are no technical restrictions for storing custom attributes in a cell. Since only supported attributes are propagated to the view, such custom attributes add no redundancy with respect to the view's attributes. (The value attribute is an exception that is an instance field, and is supported by all renderers.)
Instead of extending the DefaultGraphCell
class or using the attributes to store additional information, the class, which inherits from JTree's DefaultMutableTreeNode
also provides another mechanism to store user-defined data, namely through the user object. The user object is of type Object, and therefore provides a way to associate any object with a GraphCell
.
The user object may be in an arbitrary class hierarchy, for example extending the Hashtable
class. Since the user object's toString
method is used to provide the label, this method should probably be overridden.
A new feature in JGraph is the possibility to clone cells automatically. This feature is built into the default implementation's clipboard and cell handles, and is based on the clone
method of the Object
class, and on JGraph's cloneCells
method. The feature may be disabled using the setCloneable
method on the JGraph object. (By disabling this feature, a Control-Drag will be interpreted as a normal move.)
The process of cloning is split into a local and a global phase: In the local phase, each cell is cloned using its clone
method, returning an object that does not reference other cells. The cell and its clone are stored in a hash table, using the cell as a key and the clone as a value.
In the global phase, all cell references from a clone's original cell are replaced by references to the corresponding clones (using the before mentioned hash table). Therefore, in the process of cloning cells, first all cells are cloned using the clone
method, and then all cell references are consistently replaced by references to the respective clone.
JGraph uses the Graphics2D
class to implement its zoom. The framework is feature-aware, which means that it relies on the methods to scale a point or rectangle to screen or to model coordinates, which in turn are provided by the JGraph object. This way, the client code is independent of the actual zoom factor.
Because JGraph's zoom is implemented on top of the Graphics2D
class, the painting on the graphics object uses non-scaled coordinates (the actual scaling is done by the graphics object itself). For this reason, JGraph always returns and expects non-scaled coordinates.
For example, when implementing a MouseListener
to respond to mouse clicks, the event's point will have to be downscaled to model coordinates using the fromScreen
method in order to find the correct cell through the getFirstCellForLocation
method.
On the other hand, the original point is typically used in the component's context, for example to pop-up a menu under the mouse pointer. Make sure to clone the point that will be changed, because fromScreenmodifies the argument in-place, without creating a clone of the object. To scale from the model to screen, for example to find the position of a vertex on the component, the toScreen
method is used.
To support the grid, each point that is used in the graph must be applied to the grid using the snap
method. As in the case of zooming, the snap
method changes the argument in-place instead of cloning the point before changing it. This is because instantiation in Java is expensive, and it is not always required that the point is being cloned before it is changed. Thus, the cloning of the argument is left to the client code.
JGraph provides two additional bound properties that belong to the grid: one to make the grid visible, and the other to enable the grid. Thus, the grid can be made visible, but still be disabled, or it can be enabled and not visible.