The view in JGraph is somewhat different from other classes in Swing that carry the term view in their names. The difference is that JGraph's view is stateful, which means it contains information that is solely stored in the view. The GraphLayoutCache
object and the CellView
instances make up the view of a graph, which has an internal representation of the graph's model.
The GraphLayoutCache
object holds the cell views, namely one for each cell in the model. These views are kept in two structures, an array that holds the port views, and a list that holds the views for the root cells. Thus, ports and other cells are kept in different data structures in the graph view, whereas in the model, they are kept in the same structure. The graph view also has a reference to a hash table, which is used to provide a mapping from cells to cell views. The inverse mapping is not needed, because the cell views point to their corresponding cells.
GraphLayoutCache
extends CellMapper
, has a reference to a CellMapper
(usually this), and a CellViewFactory
, and instantiates an array of PortView
s, a List that holds the root views, and a map that is used to implement the CellMapper
interface.
The children may be accessed through the CellView
interface, and are not accessible through the GraphLayoutCache
object. The creation of new views is deferred to the CellViewFactory
. By default, JGraph is used to provide this factory, which means that the JGraph object is ultimately responsible to create the views.
The graph view's representation of the graph is different from the model's internal structure, in that it was designed with the view's operations in mind. For example, the view, in contrast to the model, makes a difference between children that implement the Port
interface, and other children. The children that implement the Port
interface are treated separately, both, as children, and with respect to storage, and are kept in a separate array.
This is because the set of available ports is often accessed independently of the rest, for example when the graph is drawn with port visibility set to true, or when the source or target of an edge is changed interactively, in which case the new ports must be found and highlighted. To find the port at a specific location, the graph view loops through the array of ports in the view, and checks if a port intersects the specified point, and if so, returns this port.
Even if the ports are always visible and painted on top of all cells, the order in which ports are stored reflects the layering of the corresponding cells in the view. This means, if two ports are on top of each other, the port that belongs to the topmost cell will be selected first.
The GraphLayoutCache
class implements the CellMapper
interface, which defines the mapping from cells to views. The interface provides two methods, one to retrieve the cell view for a given cell ñ and optionally create the view if it does not exist, using the CellViewFactory
ñ and one to associate a given cell with a newly created cell view.
The graph view provides additional useful methods that are not required by this interface, namely a method to map an array of cells to an array of views, one to get all descendants for a specific view (not including ports), and one to return the visual bounds for an array of views.
The CellMapper
interface is used in JGraph to encapsulate the most important part of a GraphLayoutCache
object, namely the mapping from GraphCell
s to CellViews
. (It also allows the graph view's mapping to be exchanged at run-time, which is needed for live-preview that is explained later.) The reverse mapping is not needed, because each CellView
instance has a reference to its corresponding GraphCell
instance.
When a cell is looked-up in the graph view in order to find its corresponding cell view, a hash table access is performed using the cell's hash code, which is provided for every Java object by the Object
class, as a key. If the cell is not associated, that is, if the hash table returns null, the view is created based on the create argument, using the CellViewFactory
interface.
The association between a cell and its view is established during the creation of the view, from within the CellViewFactory
. This is because the creating of a cell view entails the creation of its dependent views, such as children, or the source and target of an edge. Therefore, the association between the initial cell and its view must be available by the time the views of the children are created (because child views have a reference to their parent views, and edge views have a reference to their source and target port views, which are looked-up using the CellMapper
object that is passed to the createView
method upon creation of the view.)
Cell views are used to store the geometric pattern of a graph, and also to associate a renderer, editor and cell handle with a certain cell view. The renderer is responsible to paint the cell, the editor is used for in-place editing, and the cell handle is used for more sophisticated editing. For efficiency, the Flyweight pattern is used to share the renderer among all instances of a given specific CellView
class. This is achieved by making the reference to the renderer static. The editor and cell handle, instead, are created on the fly.
VertexView
, EdgeView
and PortView
extend AbstractCellView
, which in turn extends CellView
. AbstractCellView
has a static reference to the GraphCellEditor
and to the CellHandle
. Each concrete view has a static reference its corresponding subclass of CellViewRenderer
, which is used to paint the cell.
The CellView
interface is used to access the view's graph and group structure, and to hold attributes. The CellView
interface provides access to the parent and child views, and to a CellViewRenderer
, a GraphCellEditorand
a CellHandle
. The getBounds
method returns the bounds of the view, and the intersects
method is used to perform hit-detection.
The refresh
method is messaged when the corresponding cell has changed in the model, and so is the update
method, but the latter is also invoked when a dependent cell has changed, or when the view needs to be updated (for example during a live-preview, explained in Section 5.4.1, “Live-Preview”).
The update
method is a good place to implement automatic attributes, such as edge routing, that is, attributes that are computed based on other attributes or the graph's geometry. The attributes of a cell view are accessed using the getAttributes
method, with the exception of the bounds, which are cached, and accessed using the getBounds
method.
By default, the JGraph object, which acts as a CellViewFactory
is able to return cell views for each of the default implementations of the GraphCell
interface. The cell view is created based on the type of the passed-in cell. For objects that implement the Edge
interface, EdgeView
s are created, and for objects that implement the Port
interface, PortView
s are created. For other objects, VertexView
s are created.
AbstractCellView
has a reference to the parent CellView
, and an array of CellView
s that represent the views of the children. EdgeView
has a reference to the CellView
of the source and target port, and PortView
has a reference to the PortView
of the anchor. In contrast to the DefaultPort
's edge set, the PortView
does not provide access to the set of attached edges.
The AbstractCellView
class provides the basic functionality for its subclasses. For example, it implements the getRendererComponent
method to create a renderer, configure and return it. The concrete renderer is retrieved using an inner call to the getRenderer
abstract method, which must be implemented by subclassers to return a specific renderer.
The intersects
method, which is used to implement hit-detection, uses the getBounds
method to check if a given point intersects a cell view. The EdgeView
class overrides this default implementation, because in the context of an edge, a hit should only be detected if the mouse is over the shape of the edge (or its label), but not if it is inside the bounds and outside the shape.
Using the view's edit
method, the attributes of the cell views may be changed. The argument is a map, from views to attributes. The toBackand toFront
methods are used to change the layering of the cell views.
The GraphContext
class is named after the fact that each selection in a graph may possibly have a number of dependent cells, which are not selected. These cells must be treated in a special way in order to display a live-preview, that is, an exact view of how the graph will look after the current transaction. This is because for example an edge may change position when its source and target port are moved, even if the edge itself is not selected.
For example, when moving the vertex B in the above graph, its port, and the attached edge must be duplicated and repainted to reflect the new position of the vertex, even if the edge and port themselves are not selected. Thus, the selection consists of the vertex B only, and the port and edge are duplicated because they are part of the context of this selection. (In the case where the user holds down the control-key while moving cells, which is interpreted as cloning, the unselected cells are not visible, because they will not be cloned when the mouse button is released.)
The term context is used to describe the set of cells that are indirectly modified by a change, but are not themselves part of the selection. The GraphContext
object is used to compute and manage this set. Additionally, the object may be used to disconnect a set of edges from the original graph, that is, from all the ports that are not part of the GraphContext
. (Being part of the graph context either means that a cell or one of its ancestors is selected, or the cell is part of the context of this selection.)
To create a GraphContext
object, the current selection is passed to the constructor. In the constructor, the set of descendants of the selected cells is computed using the model's getDescendants
method, and stored within the GraphContext
object. Then, the set of edges that is attached to these descendants is computed using the models getEdges
method, and the set of descendants is subtracted from it, resulting in the context, that is, the set of cells that are attached, but neither being selected, nor having selected ancestors.
Having these two sets at hand, the graph context may create the temporary views for the selected cells and their context. The temporary views are typically used to provide a live-preview for the change that is currently in progress, such as a move or a resize.
For such a change, the temporary views are created using the above mechanism, and then changed in-place, until the user releases the mouse, in which case the changes are applied to the original attributes using the applyMap
method. If the user presses escape during the change, then the change is aborted, and therefore the initial attributes are not changed in this case.
The temporary views are created in two steps: First, the temporary views of the selected cells are created, and associated with their corresponding cells using a local hash table. Then, the temporary views for the context are created, using the GraphContext
instance as the CellMapper
, and also stored in this local hash table.
The mapping in the graph context uses the local hash table to look-up the references for the temporary views. That is, the views created by the graph context reference the temporary views as their parents, children or source and target ports, if such temporary views exist. (The temporary views are created lazily, when needed, based on the cell being contained in either the set of descendants, or the context, and stored in the local hash table upon creation.)
Thus, in contrast to the graph view, this cell mapper does only create a new (temporary) cell view if the cell is part of the context, and therefore, the resulting views reference the original graph's cell views if the corresponding cells are not part of the context.
These temporary views are used for live-preview, which is explained in the next chapter, because it is related with the CellHandle
s, which are used in the UI-delegate (the "control").