/*
* $Id: JGraph.java,v 1.96 2009/09/24 13:54:11 david Exp $
*
* Copyright (c) 2001-2009 JGraph Ltd
*
*/
package org.jgraph;
import java.awt.AlphaComposite;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GraphicsConfiguration;
import java.awt.Image;
import java.awt.Rectangle;
import java.awt.Transparency;
import java.awt.event.MouseEvent;
import java.awt.geom.Dimension2D;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.awt.image.VolatileImage;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import java.util.Vector;
import javax.accessibility.Accessible;
import javax.swing.BorderFactory;
import javax.swing.ImageIcon;
import javax.swing.JComponent;
import javax.swing.JViewport;
import javax.swing.Scrollable;
import javax.swing.SwingConstants;
import org.jgraph.event.GraphSelectionEvent;
import org.jgraph.event.GraphSelectionListener;
import org.jgraph.event.GraphLayoutCacheEvent.GraphLayoutCacheChange;
import org.jgraph.graph.AbstractCellView;
import org.jgraph.graph.AttributeMap;
import org.jgraph.graph.BasicMarqueeHandler;
import org.jgraph.graph.CellView;
import org.jgraph.graph.ConnectionSet;
import org.jgraph.graph.DefaultCellViewFactory;
import org.jgraph.graph.DefaultEdge;
import org.jgraph.graph.DefaultGraphCell;
import org.jgraph.graph.DefaultGraphModel;
import org.jgraph.graph.DefaultGraphSelectionModel;
import org.jgraph.graph.GraphConstants;
import org.jgraph.graph.GraphLayoutCache;
import org.jgraph.graph.GraphModel;
import org.jgraph.graph.GraphSelectionModel;
import org.jgraph.graph.PortView;
import org.jgraph.plaf.GraphUI;
import org.jgraph.plaf.basic.BasicGraphUI;
/**
* A control that displays a network of related objects using the well-known
* paradigm of a graph.
* <p>
* A JGraph object doesn't actually contain your data; it simply provides a view
* of the data. Like any non-trivial Swing component, the graph gets data by
* querying its data model.
* <p>
* JGraph displays its data by drawing individual elements. Each element
* displayed by the graph contains exactly one item of data, which is called a
* cell. A cell may either be a vertex or an edge. Vertices may have neighbours
* or not, and edges may have source and target vertices or not, depending on
* whether they are connected.
* <p>
* <strong>Creating a Graph </strong>
* <p>
* The following code creates a JGraph object:
* <p>
* JGraph graph = new JGraph(); <br>
* ... <br>
* JScrollPane graphLayoutCache = new JScrollPane(graph)
* <p>
* The code creates an instance of JGraph and puts it in a scroll pane. JGraphs
* constructor is called with no arguments in this example, which causes the
* constructor to create a sample model.
* <p>
* <strong>Editing </strong>
* <p>
* Outmoved, cloned, resized, and shaped, or connected/disconnected to or from
* other cells.
* <p>
* <strong>Keyboard Bindings </strong>
* <p>
* JGraph defines the following set of keyboard bindings:
* <p>
* <ul>
* <li>Alt-Click forces marquee selection if over a cell.
* <li>Shift- or Ctrl-Select extends or toggles the selection.
* <li>Shift-Drag constrains the offset to one direction.
* <li>Ctrl-Drag clones the selection.
* <li>Doubleclick/F2 starts editing a cell.
* </ul>
* You can change the number of clicks that triggers editing using
* setEditClickCount().
* <p>
* <strong>Customization </strong>
* <p>
* There are a number of additional methods that customize JGraph. For example,
* setMinimumMove() defines the minimum amount of pixels before a move operation
* is initiated. setSnapSize() defines the maximum distance for a cell to be
* selected. setFloatEnabled() enables/disables port floating.
* <p>
* With setDisconnectOnMove() you can indicate if the selected subgraph should
* be disconnected from the unselected rest when a move operation is initiated.
* setDragEnabled() enables/disables the use of Drag And Drop, and
* setDropEnabled() sets if the graph accepts Drops from external sources.
* <p>
* <strong>Customizing a graphs display </strong>
* <p>
* JGraph performs some look-and-feel specific painting. You can customize this
* painting in a limited way. For example, you can modify the grid using
* setGridColor() and setGridSize(), and you can change the handle colors using
* setHandleColor() and setLockedHandleColor().
* <p>
* If you want finer control over the rendering, you can subclass one of the
* default renderers, and extend its paint()-method. A renderer is a
* Component-extension that paints a cell based on its attributes. Thus, neither
* the JGraph nor its look-and-feel-specific implementation actually contain the
* code that paints the cell. Instead, the graph uses the cell renderers
* painting code.
* <p>
* <strong>Selection </strong>
* <p>
* Apart from the single-cell and marquee-selection, JGraphs selection model
* also allows to "step-into" groups, and select children. This feature can be
* disabled using the setAllowsChildSelection() method of the selection model
* instance.
* <p>
* If you are interested in knowing when the selection changes implement the
* <code>GraphSelectionListener</code> interface and add the instance using
* the method <code>addGraphSelectionListener</code>.
* <code>valueChanged</code> will be invoked when the selection changes, that
* is if the user clicks twice on the same vertex <code>valueChanged</code>
* will only be invoked once.
* <p>
* <strong>Change Notification </strong>
* <p>
* For detection of double-clicks or when a user clicks on a cell, regardless of
* whether or not it was selected, I recommend you implement a MouseListener and
* use <code>getFirstCellForLocation</code>.
* <p>
* <strong>Undo Support </strong>
* <p>
* To enable Undo-Support, a <code>GraphUndoManager</code> must be added using
* <code>addGraphSelectionListener</code>. The GraphUndoManager is an
* extension of Swing's <code>GraphUndoManager</code> that maintains a command
* history in the context of multiple views. In this setup, a cell may have a
* set of attributes in each view attached to the model.
* <p>
* For example, consider a position that is stored separately in each view. If a
* node is inserted, the change will be visible in all attached views, resulting
* in a new node that pops-up at the initial position. If the node is
* subsequently moved, say, in view1, this does not constitute a change in
* view2. If view2 does an "undo", the move <i>and </i> the insertion must be
* undone, whereas an "undo" in view1 will only undo the previous move
* operation.
* <p>
* Like all <code>JComponent</code> classes, you can use
* {@link javax.swing.InputMap}and {@link javax.swing.ActionMap}to associate
* an {@link javax.swing.Action}object with a {@link javax.swing.KeyStroke}and
* execute the action under specified conditions.
*
* @author Gaudenz Alder
* @version 2.1 16/03/03
*
*/
public class JGraph extends JComponent implements Scrollable, Accessible, Serializable {
public static final String VERSION = "JGraph (v5.13.0.0)";
public static final int DOT_GRID_MODE = 0;
public static final int CROSS_GRID_MODE = 1;
public static final int LINE_GRID_MODE = 2;
// Turn off XOR painting on MACs since it doesn't work
public static boolean IS_MAC = false;
static {
try {
String osName = System.getProperty("os.name");
if (osName != null) {
IS_MAC = osName.toLowerCase().startsWith("mac os x");
}
String javaVersion = System.getProperty("java.version");
if (javaVersion.startsWith("1.4") || javaVersion.startsWith("1.5")) {
// TODO different double buffering for 1.6 JVM?
}
} catch (Exception e) {
// ignore
}
}
/**
* @see #getUIClassID
* @see #readObject
*/
private static final String uiClassID = "GraphUI";
/** Creates a new event and passes it off the <code>selectionListeners</code>. */
protected transient GraphSelectionRedirector selectionRedirector;
//
// Bound Properties
//
/**
* The model that defines the graph displayed by this object. Bound
* property.
*/
transient protected GraphModel graphModel;
/**
* The view that defines the display properties of the model. Bound
* property.
*/
transient protected GraphLayoutCache graphLayoutCache;
/** Models the set of selected objects in this graph. Bound property. */
transient protected GraphSelectionModel selectionModel;
/** Handler for marquee selection. */
transient protected BasicMarqueeHandler marquee;
/** Off screen image for double buffering */
protected transient Image offscreen;
/** The bounds of the offscreen buffer */
protected transient Rectangle2D offscreenBounds;
/** The offset of the offscreen buffer */
protected transient Point2D offscreenOffset;
/** Graphics object of off screen image */
protected transient Graphics offgraphics;
/** Whether or not the current background image is correct */
protected transient Rectangle2D offscreenDirty = null;
protected transient boolean wholeOffscreenDirty = false;
protected transient double wholeOffscreenDirtyProportion = 0.8;
/**
* The buffer around the offscreen graphics object that provides the
* specified distance of scrolling before the buffer has to be recreated.
* Increasing the value means fewer buffer allocations but more
* memory usage for the current buffer
*/
protected transient int offscreenBuffer = 300;
/**
* Whether or not to try to use a volatile offscreen buffer for double
* buffering. Volatile
*/
protected boolean volatileOffscreen = false;
/** Stores whether the last double buffer allocation worked or not */
protected boolean lastBufferAllocated = true;
/** Holds the background image. */
protected ImageIcon backgroundImage;
/** A Component responsible for drawing the background image, if any */
protected Component backgroundComponent;
/** Whether or not the background image is scaled on zooming */
protected boolean backgroundScaled = true;
/** Scale of the graph. Default is 1. Bound property. */
protected double scale = 1.0;
/** True if the graph is anti-aliased. Default is false. Bound property. */
protected boolean antiAliased = false;
/** True if the graph allows editing the value of a cell. Bound property. */
protected boolean editable = true;
/** True if the graph allows editing of non-leaf cells. Bound property. */
protected boolean groupsEditable = false;
/**
* True if the graph allows selection of cells. Note: You must also disable
* selectNewCells if you disable this. Bound property.
*/
protected boolean selectionEnabled = true;
/**
* True if the graph allows invalid null ports during previews (aka flip
* back edges). Default is true.
*/
protected boolean previewInvalidNullPorts = true;
/** True if the grid is visible. Bound property. */
protected boolean gridVisible = false;
/** The size of the grid in points. Default is 10. Bound property. */
protected double gridSize = 10;
/** The style of the grid. Use one of the _GRID_MODE constants. */
protected int gridMode = DOT_GRID_MODE;
/** True if the ports are visible. Bound property. */
protected boolean portsVisible = false;
/** True if the ports are scaled. Bound property. */
protected boolean portsScaled = true;
/** True if port are painted above all other cells. */
protected boolean portsOnTop = true;
/** True if the graph allows to move cells below zero. */
protected boolean moveBelowZero = false;
/** True if the graph allows to move cells beyond the graph bounds */
protected boolean moveBeyondGraphBounds = true;
/** True if the labels on edges may be moved. */
protected boolean edgeLabelsMovable = true;
/**
* True if the graph should be auto resized when cells are moved below the
* bottom right corner. Default is true.
*/
protected boolean autoResizeGraph = true;
//
// Look-And-Feel dependent
//
/** Highlight Color. This color is used to draw the selection border of
* unfocused cells. Changes when the Look-and-Feel changes. */
protected Color highlightColor = Color.green;
/**
* Color of the handles and locked handles. Changes when the Look-and-Feel
* changes. This color is also used to draw the selection border
* of focused cells.
*/
protected Color handleColor, lockedHandleColor;
/** Color of the marquee. Changes when the Look-and-Feel changes. */
protected Color marqueeColor;
/** The color of the grid. This color is used to draw the selection border
* for cells with selected children. Changes when the Look-and-Feel changes. */
protected Color gridColor;
//
// Datatransfer
//
/**
* True if Drag-and-Drop should be used for move operations. Default is
* false due to a JDK bug.
*/
protected boolean dragEnabled = false;
/**
* True if the graph accepts transfers from other components (graphs). This
* also affects the clipboard. Default is true.
*/
protected boolean dropEnabled = true;
/**
* True if the graph accepts transfers from other components (graphs). This
* also affects the clipboard. Default is true.
*/
protected boolean xorEnabled = !IS_MAC;
//
// Unbound Properties
//
/** Number of clicks for editing to start. Default is 2 clicks. */
protected int editClickCount = 2;
/** True if the graph allows interactions. Default is true. */
protected boolean enabled = true;
/** True if the snap method should be active (snap to grid). */
protected boolean gridEnabled = false;
/** Size of a handle. Default is 3 pixels. */
protected int handleSize = 3;
/** Maximum distance between a cell and the mousepointer. Default is 4. */
protected int tolerance = 4;
/** Minimum amount of pixels to start a move transaction. Default is 5. */
protected int minimumMove = 5;
/**
* True if getPortViewAt should return the default port if no other port is
* found. Default is false.
*/
protected boolean isJumpToDefaultPort = false;
/**
* Specifies if cells should be added to a group when moved over the group's
* area. Default is false.
*/
protected boolean isMoveIntoGroups = false;
/**
* Specifies if cells should be removed from groups when removed from the
* group area. Default is false.
*/
protected boolean isMoveOutOfGroups = false;
/**
* True if selected edges are disconnected from unselected vertices on move.
* Default is false.
*/
protected boolean disconnectOnMove = false;
/** True if the graph allows move operations. Default is true. */
protected boolean moveable = true;
/** True if the graph allows "ctrl-drag" operations. Default is false. */
protected boolean cloneable = false;
/** True if the graph allows cells to be resized. Default is true. */
protected boolean sizeable = true;
/**
* True if the graph allows points to be modified/added/removed. Default is
* true.
*/
protected boolean bendable = true;
/**
* True if the graph allows new connections to be established. Default is
* true.
*/
protected boolean connectable = true;
/**
* True if the graph allows existing connections to be removed. Default is
* true.
*/
protected boolean disconnectable = true;
/**
* If true, when editing is to be stopped by way of selection changing, data
* in graph changing or other means <code>stopCellEditing</code> is
* invoked, and changes are saved. If false, <code>cancelCellEditing</code>
* is invoked, and changes are discarded.
*/
protected boolean invokesStopCellEditing;
//
// Bound propery names
//
/**
* Bound property name for <code>graphModel</code>.
*/
public final static String GRAPH_MODEL_PROPERTY = "model";
/**
* Bound property name for <code>graphModel</code>.
*/
public final static String GRAPH_LAYOUT_CACHE_PROPERTY = "view";
/**
* Bound property name for <code>graphModel</code>.
*/
public final static String MARQUEE_HANDLER_PROPERTY = "marquee";
/**
* Bound property name for <code>editable</code>.
*/
public final static String EDITABLE_PROPERTY = "editable";
/**
* Bound property name for <code>selectionEnabled</code>.
*/
public final static String SELECTIONENABLED_PROPERTY = "selectionEnabled";
/**
* Bound property name for <code>scale</code>.
*/
public final static String SCALE_PROPERTY = "scale";
/**
* Bound property name for <code>antiAliased</code>.
*/
public final static String ANTIALIASED_PROPERTY = "antiAliased";
/**
* Bound property name for <code>gridSize</code>.
*/
public final static String GRID_SIZE_PROPERTY = "gridSize";
/**
* Bound property name for <code>gridVisible</code>.
*/
public final static String GRID_VISIBLE_PROPERTY = "gridVisible";
/**
* Bound property name for <code>gridColor</code>.
*/
public final static String GRID_COLOR_PROPERTY = "gridColor";
/**
* Bound property name for <code>gridColor</code>.
*/
public final static String HANDLE_COLOR_PROPERTY = "handleColor";
/**
* Bound property name for <code>gridColor</code>.
*/
public final static String HANDLE_SIZE_PROPERTY = "handleSize";
/**
* Bound property name for <code>gridColor</code>.
*/
public final static String LOCKED_HANDLE_COLOR_PROPERTY = "lockedHandleColor";
/**
* Bound property name for <code>gridVisible</code>.
*/
public final static String PORTS_VISIBLE_PROPERTY = "portsVisible";
/**
* Bound property name for <code>portsScaled</code>.
*/
public final static String PORTS_SCALED_PROPERTY = "portsScaled";
/**
* Bound property name for <code>selectionModel</code>.
*/
public final static String SELECTION_MODEL_PROPERTY = "selectionModel";
/**
* Bound property name for <code>messagesStopCellEditing</code>.
*/
public final static String INVOKES_STOP_CELL_EDITING_PROPERTY = "invokesStopCellEditing";
/**
* Bound property name for <code>backgroundImage</code>.
*/
public final static String PROPERTY_BACKGROUNDIMAGE = "backgroundImage";
/**
* Creates and returns a sample <code>GraphModel</code>. Used primarily
* for beanbuilders to show something interesting.
*/
public static void addSampleData(GraphModel model) {
ConnectionSet cs = new ConnectionSet();
Map attributes = new Hashtable();
// Styles For Implement/Extend/Aggregation
AttributeMap implementStyle = new AttributeMap();
GraphConstants.setLineBegin(implementStyle,
GraphConstants.ARROW_TECHNICAL);
GraphConstants.setBeginSize(implementStyle, 10);
GraphConstants.setDashPattern(implementStyle, new float[] { 3, 3 });
if (GraphConstants.DEFAULTFONT != null) {
GraphConstants.setFont(implementStyle, GraphConstants.DEFAULTFONT
.deriveFont(10));
}
AttributeMap extendStyle = new AttributeMap();
GraphConstants
.setLineBegin(extendStyle, GraphConstants.ARROW_TECHNICAL);
GraphConstants.setBeginFill(extendStyle, true);
GraphConstants.setBeginSize(extendStyle, 10);
if (GraphConstants.DEFAULTFONT != null) {
GraphConstants.setFont(extendStyle, GraphConstants.DEFAULTFONT
.deriveFont(10));
}
AttributeMap aggregateStyle = new AttributeMap();
GraphConstants.setLineBegin(aggregateStyle,
GraphConstants.ARROW_DIAMOND);
GraphConstants.setBeginFill(aggregateStyle, true);
GraphConstants.setBeginSize(aggregateStyle, 6);
GraphConstants.setLineEnd(aggregateStyle, GraphConstants.ARROW_SIMPLE);
GraphConstants.setEndSize(aggregateStyle, 8);
GraphConstants.setLabelPosition(aggregateStyle, new Point2D.Double(500,
0));
if (GraphConstants.DEFAULTFONT != null) {
GraphConstants.setFont(aggregateStyle, GraphConstants.DEFAULTFONT
.deriveFont(10));
}
//
// The Swing MVC Pattern
//
// Model Column
DefaultGraphCell gm = new DefaultGraphCell("GraphModel");
attributes.put(gm,
createBounds(new AttributeMap(), 20, 100, Color.blue));
gm.addPort(null, "GraphModel/Center");
DefaultGraphCell dgm = new DefaultGraphCell("DefaultGraphModel");
attributes.put(dgm, createBounds(new AttributeMap(), 20, 180,
Color.blue));
dgm.addPort(null, "DefaultGraphModel/Center");
DefaultEdge dgmImplementsGm = new DefaultEdge("implements");
cs.connect(dgmImplementsGm, gm.getChildAt(0), dgm.getChildAt(0));
attributes.put(dgmImplementsGm, implementStyle);
DefaultGraphCell modelGroup = new DefaultGraphCell("ModelGroup");
modelGroup.add(gm);
modelGroup.add(dgm);
modelGroup.add(dgmImplementsGm);
// JComponent Column
DefaultGraphCell jc = new DefaultGraphCell("JComponent");
attributes.put(jc, createBounds(new AttributeMap(), 180, 20,
Color.green));
jc.addPort(null, "JComponent/Center");
DefaultGraphCell jg = new DefaultGraphCell("JGraph");
attributes.put(jg, createBounds(new AttributeMap(), 180, 100,
Color.green));
jg.addPort(null, "JGraph/Center");
DefaultEdge jgExtendsJc = new DefaultEdge("extends");
cs.connect(jgExtendsJc, jc.getChildAt(0), jg.getChildAt(0));
attributes.put(jgExtendsJc, extendStyle);
// UI Column
DefaultGraphCell cu = new DefaultGraphCell("ComponentUI");
attributes
.put(cu, createBounds(new AttributeMap(), 340, 20, Color.red));
cu.addPort(null, "ComponentUI/Center");
DefaultGraphCell gu = new DefaultGraphCell("GraphUI");
attributes.put(gu,
createBounds(new AttributeMap(), 340, 100, Color.red));
gu.addPort(null, "GraphUI/Center");
DefaultGraphCell dgu = new DefaultGraphCell("BasicGraphUI");
attributes.put(dgu, createBounds(new AttributeMap(), 340, 180,
Color.red));
dgu.addPort(null, "BasicGraphUI/Center");
DefaultEdge guExtendsCu = new DefaultEdge("extends");
cs.connect(guExtendsCu, cu.getChildAt(0), gu.getChildAt(0));
attributes.put(guExtendsCu, extendStyle);
DefaultEdge dguImplementsDu = new DefaultEdge("implements");
cs.connect(dguImplementsDu, gu.getChildAt(0), dgu.getChildAt(0));
attributes.put(dguImplementsDu, implementStyle);
DefaultGraphCell uiGroup = new DefaultGraphCell("UIGroup");
uiGroup.add(cu);
uiGroup.add(gu);
uiGroup.add(dgu);
uiGroup.add(dguImplementsDu);
uiGroup.add(guExtendsCu);
// Aggregations
DefaultEdge jgAggregatesGm = new DefaultEdge("model");
cs.connect(jgAggregatesGm, jg.getChildAt(0), gm.getChildAt(0));
attributes.put(jgAggregatesGm, aggregateStyle);
DefaultEdge jcAggregatesCu = new DefaultEdge("ui");
cs.connect(jcAggregatesCu, jc.getChildAt(0), cu.getChildAt(0));
attributes.put(jcAggregatesCu, aggregateStyle);
// Insert Cells into model
Object[] cells = new Object[] { jgAggregatesGm, jcAggregatesCu,
modelGroup, jc, jg, jgExtendsJc, uiGroup };
model.insert(cells, attributes, cs, null, null);
}
/**
* Returns an attributeMap for the specified position and color.
*/
public static Map createBounds(AttributeMap map, int x, int y, Color c) {
GraphConstants.setBounds(map, map.createRect(x, y, 90, 30));
GraphConstants.setBorder(map, BorderFactory.createRaisedBevelBorder());
GraphConstants.setBackground(map, c.darker().darker());
GraphConstants
.setGradientColor(map, c.brighter().brighter().brighter());
GraphConstants.setForeground(map, Color.white);
if (GraphConstants.DEFAULTFONT != null) {
GraphConstants.setFont(map, GraphConstants.DEFAULTFONT.deriveFont(
Font.BOLD, 12));
}
GraphConstants.setOpaque(map, true);
return map;
}
/**
* Returns a <code>JGraph</code> with a sample model.
*/
public JGraph() {
this((GraphModel) null);
}
/**
* Returns an instance of <code>JGraph</code> which displays the the
* specified data model.
*
* @param model
* the <code>GraphModel</code> to use as the data model
*/
public JGraph(GraphModel model) {
this(model, (GraphLayoutCache) null);
}
/**
* Returns an instance of <code>JGraph</code> which displays the data
* model using the specified view.
*
* @param cache
* the <code>GraphLayoutCache</code> to use as the view
*/
public JGraph(GraphLayoutCache cache) {
this((cache != null) ? cache.getModel() : null, cache);
}
/**
* Returns an instance of <code>JGraph</code> which displays the specified
* data model using the specified view.
*
* @param model
* the <code>GraphModel</code> to use as the data model
* @param cache
* the <code>GraphLayoutCache</code> to use as the cache
*/
public JGraph(GraphModel model, GraphLayoutCache cache) {
this(model, cache, new BasicMarqueeHandler());
}
/**
* Returns an instance of <code>JGraph</code> which displays the specified
* data model and assigns the specified marquee handler
*
* @param model
* the <code>GraphModel</code> to use as the data model
* @param mh
* the <code>BasicMarqueeHandler</code> to use as the marquee
* handler
*/
public JGraph(GraphModel model, BasicMarqueeHandler mh) {
this(model, null, mh);
}
/**
* Returns an instance of <code>JGraph</code> which displays the specified
* data model using the specified view and assigns the specified marquee
* handler
*
* @param model
* the <code>GraphModel</code> to use as the data model
* @param layoutCache
* the <code>GraphLayoutCache</code> to use as the cache
* @param mh
* the <code>BasicMarqueeHandler</code> to use as the marquee
* handler
*/
public JGraph(GraphModel model, GraphLayoutCache layoutCache,
BasicMarqueeHandler mh) {
setDoubleBuffered(true);
selectionModel = new DefaultGraphSelectionModel(this);
setLayout(null);
marquee = mh;
if (model == null) {
model = new DefaultGraphModel();
setModel(model);
addSampleData(model);
} else
setModel(model);
if (layoutCache == null)
layoutCache = new GraphLayoutCache(model,
new DefaultCellViewFactory());
setGraphLayoutCache(layoutCache);
updateUI();
}
//
// UI-delegate (GraphUI)
//
/**
* Returns the L&F object that renders this component.
*
* @return the GraphUI object that renders this component
*/
public GraphUI getUI() {
return (GraphUI) ui;
}
/**
* Sets the L&F object that renders this component.
*
* @param ui
* the GraphUI L&F object
* @see javax.swing.UIDefaults#getUI(JComponent)
*
*/
public void setUI(GraphUI ui) {
if ((GraphUI) this.ui != ui) {
super.setUI(ui);
}
}
/**
* Notification from the <code>UIManager</code> that the L&F has changed.
* Replaces the current UI object with the latest version from the
* <code>UIManager</code>. Subclassers can override this to support
* different GraphUIs.
*
* @see JComponent#updateUI
*
*/
public void updateUI() {
setUI(new org.jgraph.plaf.basic.BasicGraphUI());
invalidate();
}
/**
* Returns the name of the L&F class that renders this component.
*
* @return the string "GraphUI"
* @see JComponent#getUIClassID
*
*/
public String getUIClassID() {
return uiClassID;
}
//
// Content
//
/**
* Returns all root cells (cells that have no parent) that the model
* contains.
*/
public Object[] getRoots() {
return DefaultGraphModel.getRoots(graphModel);
}
/**
* Returns all cells that intersect the given rectangle.
*/
public Object[] getRoots(Rectangle clip) {
CellView[] views = graphLayoutCache.getRoots(clip);
Object[] cells = new Object[views.length];
for (int i = 0; i < views.length; i++)
cells[i] = views[i].getCell();
return cells;
}
/**
* Returns all <code>cells</code> including all descendants in the passed
* in order of cells.
*/
public Object[] getDescendants(Object[] cells) {
return DefaultGraphModel.getDescendants(getModel(), cells).toArray();
}
/**
* Returns all <code>cells</code> including all descendants ordered using
* the current layering data stored by the model.
*/
public Object[] order(Object[] cells) {
return DefaultGraphModel.order(getModel(), cells);
}
/**
* Returns a map of (cell, clone)-pairs for all <code>cells</code> and
* their children. Special care is taken to replace the anchor references
* between ports. (Iterative implementation.)
*/
public Map cloneCells(Object[] cells) {
return graphModel.cloneCells(cells);
}
/**
* Returns the topmost cell view at the specified location using the view's
* bounds on non-leafs to check for containment. If reverse is true this
* will return the innermost view.
*/
public CellView getTopmostViewAt(double x, double y, boolean reverse,
boolean leafsOnly) {
Rectangle2D r = new Rectangle2D.Double(x, y, 1, 1);
Object[] cells = getDescendants(getRoots());
for (int i = (reverse) ? cells.length - 1 : 0; i >= 0
&& i < cells.length; i += (reverse) ? -1 : +1) {
CellView view = getGraphLayoutCache().getMapping(cells[i], false);
if (view != null
&& (!leafsOnly || view.isLeaf())
&& ((view.isLeaf() && view.intersects(this, r)) || (!view
.isLeaf() && view.getBounds().contains(x, y))))
return view;
}
return null;
}
/**
* Returns the topmost cell at the specified location.
*
* @param x
* an integer giving the number of pixels horizontally from the
* left edge of the display area, minus any left margin
* @param y
* an integer giving the number of pixels vertically from the top
* of the display area, minus any top margin
* @return the topmost cell at the specified location
*/
public Object getFirstCellForLocation(double x, double y) {
return getNextCellForLocation(null, x, y);
}
/**
* Returns the cell at the specified location that is "behind" the
* <code>current</code> cell. Returns the topmost cell if there are no
* more cells behind <code>current</code>. Note: This does only return
* visible cells.
*/
public Object getNextCellForLocation(Object current, double x, double y) {
CellView cur = graphLayoutCache.getMapping(current, false);
CellView cell = getNextViewAt(cur, x, y);
if (cell != null)
return cell.getCell();
return null;
}
/**
* Returns the bounding rectangle of the specified cell.
*/
public Rectangle2D getCellBounds(Object cell) {
CellView view = graphLayoutCache.getMapping(cell, false);
if (view != null)
return view.getBounds();
return null;
}
/**
* Returns the bounding rectangle of the specified cells.
*/
public Rectangle2D getCellBounds(Object[] cells) {
if (cells != null && cells.length > 0) {
Rectangle2D r = getCellBounds(cells[0]);
Rectangle2D ret = (r != null) ? (Rectangle2D) r.clone() : null;
for (int i = 1; i < cells.length; i++) {
r = getCellBounds(cells[i]);
if (r != null) {
if (ret == null)
ret = (r != null) ? (Rectangle2D) r.clone() : null;
else
Rectangle2D.union(ret, r, ret);
}
}
return ret;
}
return null;
}
/**
* Returns the next view at the specified location wrt. <code>current</code>.
* This is used to iterate overlapping cells, and cells that are grouped.
* The current selection affects this method. <br>
* Note: This returns the next <i>selectable </i> view. <br>
* Note: Arguments are not expected to be scaled (they are scaled in here).
*/
public CellView getNextViewAt(CellView current, double x, double y) {
return getNextViewAt(current, x, y, false);
}
/**
* Returns the next view at the specified location wrt. <code>current</code>.
* This is used to iterate overlapping cells, and cells that are grouped.
* The current selection affects this method. <br>
* Note: This returns the next <i>selectable </i> view. <br>
* Note: Arguments are not expected to be scaled (they are scaled in here).
*/
public CellView getNextViewAt(CellView current, double x, double y,
boolean leafsOnly) {
CellView[] cells = AbstractCellView
.getDescendantViews(getGraphLayoutCache().getRoots());
return getNextViewAt(cells, current, x, y, leafsOnly);
}
/**
* Note: Arguments are not expected to be scaled (they are scaled in here).
*/
public CellView getNextSelectableViewAt(CellView current, double x, double y) {
CellView[] selectables = getGraphLayoutCache().getMapping(
getSelectionModel().getSelectables(), false);
return getNextViewAt(selectables, current, x, y);
}
/**
* Returns the next view at the specified location wrt. <code>c</code> in
* the specified array of views. The views must be in order, as returned,
* for example, by GraphLayoutCache.order(Object[]).
*/
public CellView getNextViewAt(CellView[] cells, CellView c, double x,
double y) {
return getNextViewAt(cells, c, x, y, false);
}
/**
* Returns the next view at the specified location wrt. <code>c</code> in
* the specified array of views. The views must be in order, as returned,
* for example, by GraphLayoutCache.order(Object[]).
*/
public CellView getNextViewAt(CellView[] cells, CellView c, double x,
double y, boolean leafsOnly) {
if (cells != null) {
Rectangle2D r = fromScreen(new Rectangle2D.Double(x - tolerance, y
- tolerance, 2 * tolerance, 2 * tolerance));
// Ensure the tolerance is at least 1.0 (can go below in high zoom
// in case).
if (r.getWidth() < 1.0) {
r.setFrame(r.getX(), r.getY(), 1.0, r.getHeight());
}
if (r.getHeight() < 1.0) {
r.setFrame(r.getX(), r.getY(), r.getWidth(), 1.0);
}
// Iterate through cells and switch to active
// if current is traversed. Cache first cell.
CellView first = null;
boolean active = (c == null);
for (int i = 0; i < cells.length; i++) {
if (cells[i] != null && (!leafsOnly || cells[i].isLeaf())
&& cells[i].intersects(this, r)) {
// TODO: This behaviour is specific to selection and
// should be parametrized (it only returns a group with
// selected children if no other portview is available)
if (active
&& !selectionModel.isChildrenSelected(cells[i]
.getCell())) {
return cells[i];
} else if (first == null)
first = cells[i];
active = active | (cells[i] == c);
}
}
return first;
}
return null;
}
/**
* Returns the next view at the specified location wrt. <code>c</code> in
* the specified array of views. The views must be in order, as returned,
* for example, by GraphLayoutCache.order(Object[]).
*/
public CellView getLeafViewAt(double x, double y) {
return getNextViewAt(null, x, y, true);
}
/**
* Convenience method to return the port at the specified location.
*/
public Object getPortForLocation(double x, double y) {
PortView view = getPortViewAt(x, y, tolerance);
if (view != null)
return view.getCell();
return null;
}
/**
* Returns the portview at the specified location. <br>
* Note: Arguments are not expected to be scaled (they are scaled in here).
*/
public PortView getPortViewAt(double x, double y) {
return getPortViewAt(x, y, tolerance);
}
/**
* Returns the portview at the specified location. <br>
* Note: Arguments are not expected to be scaled (they are scaled in here).
*/
public PortView getPortViewAt(double x, double y, int tolerance) {
double sx = x / scale;
double sy = y / scale;
Rectangle2D r = new Rectangle2D.Double(sx - tolerance, sy - tolerance,
2 * tolerance, 2 * tolerance);
PortView[] ports = graphLayoutCache.getPorts();
if (ports != null) {
for (int i = ports.length - 1; i >= 0; i--)
if (ports[i] != null && ports[i].intersects(this, r))
return ports[i];
if (isJumpToDefaultPort()) {
CellView cellView = getNextViewAt(null, x, y, true);
// Finds a non-edge cell under the mousepointer
if (cellView != null && graphModel.isEdge(cellView.getCell())) {
CellView nextView = getNextViewAt(cellView, x, y, true);
while (nextView != cellView
&& graphModel.isEdge(nextView.getCell())) {
nextView = getNextViewAt(nextView, x, y, true);
}
cellView = nextView;
}
if (cellView != null) {
PortView defaultPort = getDefaultPortForCell(cellView
.getCell());
return defaultPort;
}
}
}
return null;
}
/**
* Returns the default portview for the specified cell. The default
* implementation returns the first floating port (ie. the first port that
* does not define an offset) or <b>the </b> port, if there is only one
* port.
*
* @param cell
* the cell whose port is to be obtained
* @return the port view of the specified cell
*/
public PortView getDefaultPortForCell(Object cell) {
if (cell != null && !getModel().isEdge(cell)) {
int childCount = getModel().getChildCount(cell);
for (int i = 0; i < childCount; i++) {
Object childCell = getModel().getChild(cell, i);
CellView child = getGraphLayoutCache().getMapping(childCell,
false);
if (child instanceof PortView) {
Point2D offset = GraphConstants.getOffset(child
.getAllAttributes());
if (offset == null || childCount == 1)
return (PortView) child;
}
}
}
return null;
}
/**
* Converts the specified value to string. If the value is an instance of
* CellView then the corresponding value or cell is used.
*/
public String convertValueToString(Object value) {
if (value instanceof CellView)
value = ((CellView) value).getCell();
return String.valueOf(value);
}
//
// Grid and Scale
//
/**
* Returns the given point applied to the grid.
*
* @param p
* a point in screen coordinates.
* @return the same point applied to the grid.
*/
public Point2D snap(Point2D p) {
if (gridEnabled && p != null) {
double sgs = gridSize * getScale();
p.setLocation(Math.round(Math.round(p.getX() / sgs) * sgs), Math
.round(Math.round(p.getY() / sgs) * sgs));
}
return p;
}
/**
* Returns the given rectangle applied to the grid.
*
* @param r
* a rectangle in screen coordinates.
* @return the same rectangle applied to the grid.
*/
public Rectangle2D snap(Rectangle2D r) {
if (gridEnabled && r != null) {
double sgs = gridSize * getScale();
r.setFrame(Math.round(Math.round(r.getX() / sgs) * sgs), Math
.round(Math.round(r.getY() / sgs) * sgs), 1 + Math
.round(Math.round(r.getWidth() / sgs) * sgs), 1 + Math
.round(Math.round(r.getHeight() / sgs) * sgs));
}
return r;
}
/**
* Returns the given dimension applied to the grid.
*
* @param d
* a dimension in screen coordinates to snap to.
* @return the same dimension applied to the grid.
*/
public Dimension2D snap(Dimension2D d) {
if (gridEnabled && d != null) {
double sgs = gridSize * getScale();
d.setSize(1 + Math.round(Math.round(d.getWidth() / sgs) * sgs),
1 + Math.round(Math.round(d.getHeight() / sgs) * sgs));
}
return d;
}
/**
* Upscale the given point in place, using the given instance.
*
* @param p
* the point to be upscaled
* @return the upscaled point instance
*/
public Point2D toScreen(Point2D p) {
if (p == null)
return null;
p.setLocation(Math.round(p.getX() * scale), Math
.round(p.getY() * scale));
return p;
}
/**
* Downscale the given point in place, using the given instance.
*
* @param p
* the point to be downscaled
* @return the downscaled point instance
*/
public Point2D fromScreen(Point2D p) {
if (p == null)
return null;
p.setLocation(Math.round(p.getX() / scale), Math
.round(p.getY() / scale));
return p;
}
/**
* Upscale the given rectangle in place, using the given instance.
*
* @param rect
* the rectangle to be upscaled
* @return the upscaled rectangle instance
*/
public Rectangle2D toScreen(Rectangle2D rect) {
if (rect == null)
return null;
rect.setFrame(rect.getX() * scale, rect.getY() * scale, rect.getWidth()
* scale, rect.getHeight() * scale);
return rect;
}
/**
* Downscale the given rectangle in place, using the given instance.
*
* @param rect
* the rectangle to be downscaled
* @return the down-scaled rectangle instance
*/
public Rectangle2D fromScreen(Rectangle2D rect) {
if (rect == null)
return null;
rect.setFrame(rect.getX() / scale, rect.getY() / scale, rect.getWidth()
/ scale, rect.getHeight() / scale);
return rect;
}
/**
* Computes and updates the size for <code>view</code>.
*/
public void updateAutoSize(CellView view) {
if (view != null && !isEditing()) {
Rectangle2D bounds = (view.getAttributes() != null) ? GraphConstants
.getBounds(view.getAttributes())
: null;
AttributeMap attrs = getModel().getAttributes(view.getCell());
if (bounds == null)
bounds = GraphConstants.getBounds(attrs);
if (bounds != null) {
boolean autosize = GraphConstants.isAutoSize(view
.getAllAttributes());
boolean resize = GraphConstants.isResize(view
.getAllAttributes());
if (autosize || resize) {
Dimension2D d = getUI().getPreferredSize(this, view);
bounds.setFrame(bounds.getX(), bounds.getY(), d.getWidth(),
d.getHeight());
// Remove resize attribute
snap(bounds);
if (resize) {
if (view.getAttributes() != null)
view.getAttributes().remove(GraphConstants.RESIZE);
attrs.remove(GraphConstants.RESIZE);
}
view.refresh(getGraphLayoutCache(), getGraphLayoutCache(), false);
}
}
}
}
/**
* Returns the attributes for the specified cell. If the layout cache
* returns a view for the cell then this method returns allAttributes,
* otherwise the method returns model.getAttributes(cell).
*/
public AttributeMap getAttributes(Object cell) {
AttributeMap attrs;
CellView cellView = getGraphLayoutCache().getMapping(cell, false);
if (cellView != null) {
attrs = cellView.getAllAttributes();
} else {
attrs = getModel().getAttributes(cell);
}
return attrs;
}
//
// Unbound Properties
//
/**
* Returns the number of clicks for editing to start.
*/
public int getEditClickCount() {
return editClickCount;
}
/**
* Sets the number of clicks for editing to start.
*/
public void setEditClickCount(int count) {
editClickCount = count;
}
/**
* Returns true if the graph accepts drops/pastes from external sources.
*/
public boolean isDropEnabled() {
return dropEnabled;
}
/**
* Sets if the graph accepts drops/pastes from external sources.
*/
public void setDropEnabled(boolean flag) {
dropEnabled = flag;
}
/**
* Returns true if the graph accepts drops/pastes from external sources.
*/
public boolean isXorEnabled() {
return (xorEnabled && isOpaque());
}
/**
* Sets if the graph accepts drops/pastes from external sources.
*/
public void setXorEnabled(boolean flag) {
xorEnabled = flag;
}
/**
* Returns true if the graph uses Drag-and-Drop to move cells.
*/
public boolean isDragEnabled() {
return dragEnabled;
}
/**
* Sets if the graph uses Drag-and-Drop to move cells.
*/
public void setDragEnabled(boolean flag) {
dragEnabled = flag;
}
/*
* Returns true if the graph allows movement of cells.
*/
public boolean isMoveable() {
return moveable;
}
/**
* Sets if the graph allows movement of cells.
*/
public void setMoveable(boolean flag) {
moveable = flag;
}
/**
* Returns true if the graph allows adding/removing/modifying points.
*/
public boolean isBendable() {
return bendable;
}
/**
* Sets if the graph allows adding/removing/modifying points.
*/
public void setBendable(boolean flag) {
bendable = flag;
}
/**
* Returns true if the graph allows new connections to be established.
*/
public boolean isConnectable() {
return connectable;
}
/**
* Setse if the graph allows new connections to be established.
*/
public void setConnectable(boolean flag) {
connectable = flag;
}
/**
* Returns true if the graph allows existing connections to be removed.
*/
public boolean isDisconnectable() {
return disconnectable;
}
/**
* Sets if the graph allows existing connections to be removed.
*/
public void setDisconnectable(boolean flag) {
disconnectable = flag;
}
/**
* Returns true if cells are cloned on CTRL-Drag operations.
*/
public boolean isCloneable() {
return cloneable;
}
/**
* Sets if cells are cloned on CTRL-Drag operations.
*/
public void setCloneable(boolean flag) {
cloneable = flag;
}
/**
* Returns true if the graph allows cells to be resized.
*/
public boolean isSizeable() {
return sizeable;
}
/**
* Sets if the graph allows cells to be resized.
*/
public void setSizeable(boolean flag) {
sizeable = flag;
}
/**
* Sets if selected edges should be disconnected from unselected vertices
* when they are moved.
*/
public void setDisconnectOnMove(boolean flag) {
disconnectOnMove = flag;
}
/**
* Returns true if selected edges should be disconnected from unselected
* vertices when they are moved.
*/
public boolean isDisconnectOnMove() {
return disconnectOnMove && disconnectable;
}
/**
* Sets if getPortViewAt should return the default port if no other port is
* found.
*/
public void setJumpToDefaultPort(boolean flag) {
isJumpToDefaultPort = flag;
}
/**
* Returns true if getPortViewAt should return the default port if no other
* port is found.
*/
public boolean isJumpToDefaultPort() {
return isJumpToDefaultPort;
}
/**
* Specifies if cells should be added to groups when moved over the group's
* area.
*/
public void setMoveIntoGroups(boolean flag) {
isMoveIntoGroups = flag;
}
/**
* Returns true if cells should be added to groups when moved over the
* group's area.
*/
public boolean isMoveIntoGroups() {
return isMoveIntoGroups;
}
/**
* Specifies if cells should be removed from groups when removed from the
* group's area.
*/
public void setMoveOutOfGroups(boolean flag) {
isMoveOutOfGroups = flag;
}
/**
* Returns true if cells should be removed from groups when removed from the
* group's area.
*/
public boolean isMoveOutOfGroups() {
return isMoveOutOfGroups;
}
/**
* Returns true if the grid is active.
*
* @see #snap(Point2D)
*
*/
public boolean isGridEnabled() {
return gridEnabled;
}
/**
* If set to true, the grid will be active.
*
* @see #snap(Point2D)
*
*/
public void setGridEnabled(boolean flag) {
gridEnabled = flag;
}
/**
* Returns true if the graph allows to move cells below zero.
*/
public boolean isMoveBelowZero() {
return moveBelowZero;
}
/**
* Sets if the graph should auto resize when cells are being moved below the
* bottom right corner.
*/
public void setMoveBelowZero(boolean moveBelowZero) {
this.moveBelowZero = moveBelowZero;
}
/**
* @return the moveBeyondGraphBounds
*/
public boolean isMoveBeyondGraphBounds() {
return moveBeyondGraphBounds;
}
/**
* @param moveBeyondGraphBounds the moveBeyondGraphBounds to set
*/
public void setMoveBeyondGraphBounds(boolean moveBeyondGraphBounds) {
this.moveBeyondGraphBounds = moveBeyondGraphBounds;
}
/**
* Returns true if edge labels may be dragged and dropped.
*
* @return whether edge labels may be dragged and dropped
*/
public boolean getEdgeLabelsMovable() {
return edgeLabelsMovable;
}
/**
* Set if edge labels may be moved with the mouse or not.
*
* @param edgeLabelsMovable
* true if edge labels may be dragged
*/
public void setEdgeLabelsMovable(boolean edgeLabelsMovable) {
this.edgeLabelsMovable = edgeLabelsMovable;
}
/**
* Returns true if the graph should be automatically resized when cells are
* being moved below the bottom right corner. Note if the value of
* <code>moveBeyondGraphBounds</code> if <code>false</code> auto resizing
* is automatically disabled
*/
public boolean isAutoResizeGraph() {
if (!moveBeyondGraphBounds) {
return false;
}
return autoResizeGraph;
}
/**
* Sets whether or not the graph should be automatically resize when cells
* are being moved below the bottom right corner
*/
public void setAutoResizeGraph(boolean autoResizeGraph) {
this.autoResizeGraph = autoResizeGraph;
}
/**
* Returns the maximum distance between the mousepointer and a cell to be
* selected.
*/
public int getTolerance() {
return tolerance;
}
/**
* Sets the maximum distance between the mousepointer and a cell to be
* selected.
*/
public void setTolerance(int size) {
if (size < 1) {
size = 1;
}
tolerance = size;
}
/**
* Returns the size of the handles.
*/
public int getHandleSize() {
return handleSize;
}
/**
* Sets the size of the handles.
*/
public void setHandleSize(int size) {
int oldValue = handleSize;
handleSize = size;
firePropertyChange(HANDLE_SIZE_PROPERTY, oldValue, size);
}
/**
* Returns the miminum amount of pixels for a move operation.
*/
public int getMinimumMove() {
return minimumMove;
}
/**
* Sets the miminum amount of pixels for a move operation.
*/
public void setMinimumMove(int pixels) {
minimumMove = pixels;
}
//
// Laf-Specific color scheme. These colors are changed
// by BasicGraphUI when the laf changes.
//
/**
* Returns the current grid color.
*/
public Color getGridColor() {
return gridColor;
}
/**
* Sets the current grid color.
*/
public void setGridColor(Color newColor) {
Color oldValue = gridColor;
gridColor = newColor;
firePropertyChange(GRID_COLOR_PROPERTY, oldValue, newColor);
}
/**
* Returns the current handle color.
*/
public Color getHandleColor() {
return handleColor;
}
/**
* Sets the current handle color.
*/
public void setHandleColor(Color newColor) {
Color oldValue = handleColor;
handleColor = newColor;
firePropertyChange(HANDLE_COLOR_PROPERTY, oldValue, newColor);
}
/**
* Returns the current second handle color.
*/
public Color getLockedHandleColor() {
return lockedHandleColor;
}
/**
* Sets the current second handle color.
*/
public void setLockedHandleColor(Color newColor) {
Color oldValue = lockedHandleColor;
lockedHandleColor = newColor;
firePropertyChange(LOCKED_HANDLE_COLOR_PROPERTY, oldValue, newColor);
}
/**
* Returns the current marquee color.
*/
public Color getMarqueeColor() {
return marqueeColor;
}
/**
* Sets the current marquee color.
*/
public void setMarqueeColor(Color newColor) {
marqueeColor = newColor;
}
/**
* Returns the current highlight color.
*/
public Color getHighlightColor() {
return highlightColor;
}
/**
* Sets the current selection highlight color.
*/
public void setHighlightColor(Color newColor) {
highlightColor = newColor;
}
//
// Bound properties
//
/**
* Returns the current scale.
*
* @return the current scale as a double
*/
public double getScale() {
return scale;
}
/**
* Sets the current scale.
* <p>
* Fires a property change for the SCALE_PROPERTY.
*
* @param newValue
* the new scale
*/
public void setScale(double newValue) {
Point2D centerPoint = getCenterPoint();
setScale(newValue, centerPoint);
}
/**
* Sets the current scale and centers the graph to the specified point
*
* @param newValue
* the new scale
* @param center
* the center of the graph
*/
public void setScale(double newValue, Point2D center) {
if (newValue > 0 && newValue != this.scale) {
Rectangle2D view = getViewPortBounds();
double oldValue = this.scale;
scale = newValue;
boolean zoomIn = true;
Rectangle newView = null;
clearOffscreen();
if (view != null) {
double scaleRatio = newValue / oldValue;
int newCenterX = (int) (center.getX() * scaleRatio);
int newCenterY = (int) (center.getY() * scaleRatio);
int newX = (int) (newCenterX - view.getWidth() / 2.0);
int newY = (int) (newCenterY - view.getHeight() / 2.0);
newView = new Rectangle(newX, newY, (int) view.getWidth(),
(int) view.getHeight());
// When zooming out scroll before revalidation otherwise
// revalidation causes one scroll and scrollRectToVisible
// another
if (scaleRatio < 1.0) {
scrollRectToVisible(newView);
zoomIn = false;
}
}
firePropertyChange(SCALE_PROPERTY, oldValue, newValue);
// When zooming in, do it after the revalidation otherwise
// it intermittently moves to the old co-ordinate system
if (zoomIn && newView != null) {
scrollRectToVisible(newView);
}
}
}
/**
* Invalidate the offscreen region, do not just delete it, since if the new
* region is smaller than the old you may not wish to re-create the buffer
*/
public void clearOffscreen() {
if (offscreen != null) {
int h = offscreen.getHeight(this);
int w = offscreen.getWidth(this);
Rectangle2D dirtyRegion = new Rectangle2D.Double(0, 0, w, h);
fromScreen(dirtyRegion);
addOffscreenDirty(dirtyRegion);
}
}
/**
* Returns the center of the component relative to the parent viewport's
* position.
*/
public Point2D getCenterPoint() {
Rectangle2D viewBounds = getViewPortBounds();
if (viewBounds != null) {
return new Point2D.Double(viewBounds.getCenterX(), viewBounds
.getCenterY());
}
viewBounds = getBounds();
return new Point2D.Double(viewBounds.getCenterX(), viewBounds
.getCenterY());
}
/**
* Return the bounds of the parent viewport, if one exists. If one does not
* exist, null is returned
*
* @return the bounds of the parent viewport
*/
public Rectangle2D getViewPortBounds() {
if (getParent() instanceof JViewport) {
return ((JViewport) getParent()).getViewRect();
}
return null;
}
/**
* Returns the size of the grid in pixels.
*
* @return the size of the grid as an int
*/
public double getGridSize() {
return gridSize;
}
/**
* Returns the current grid view mode.
*/
public int getGridMode() {
return gridMode;
}
/**
* Sets the size of the grid.
* <p>
* Fires a property change for the GRID_SIZE_PROPERTY.
*
* @param newSize
* the new size of the grid in pixels
*/
public void setGridSize(double newSize) {
double oldValue = this.gridSize;
this.gridSize = newSize;
firePropertyChange(GRID_SIZE_PROPERTY, oldValue, newSize);
}
/**
* Sets the current grid view mode.
*
* @param mode
* The current grid view mode. Valid values are <CODE>
* DOT_GRID_MODE</CODE>,<CODE>CROSS_GRID_MODE</CODE>, and
* <CODE>LINE_GRID_MODE</CODE>.
*/
public void setGridMode(int mode) {
if (mode == DOT_GRID_MODE || mode == CROSS_GRID_MODE
|| mode == LINE_GRID_MODE) {
gridMode = mode;
repaint();
}
}
/**
* Returns true if the grid will be visible.
*
* @return true if the grid is visible
*/
public boolean isGridVisible() {
return gridVisible;
}
/**
* If set to true, the grid will be visible.
* <p>
* Fires a property change for the GRID_VISIBLE_PROPERTY.
*/
public void setGridVisible(boolean flag) {
boolean oldValue = gridVisible;
gridVisible = flag;
// Clear the double buffer if the grid has been enabled
if (flag != oldValue) {
clearOffscreen();
}
firePropertyChange(GRID_VISIBLE_PROPERTY, oldValue, flag);
}
/**
* Returns true if the ports will be visible.
*
* @return true if the ports are visible
*/
public boolean isPortsVisible() {
return portsVisible;
}
/**
* If set to true, the ports will be visible.
* <p>
* Fires a property change for the PORTS_VISIBLE_PROPERTY.
*/
public void setPortsVisible(boolean flag) {
boolean oldValue = portsVisible;
portsVisible = flag;
// Clear the double buffer if the grid has been enabled
if (flag != oldValue) {
clearOffscreen();
}
firePropertyChange(PORTS_VISIBLE_PROPERTY, oldValue, flag);
}
/**
* Returns true if the ports will be scaled.
*
* @return true if the ports are visible
*/
public boolean isPortsScaled() {
return portsScaled;
}
/**
* If set to true, the ports will be scaled.
* <p>
* Fires a property change for the PORTS_SCALED_PROPERTY.
*/
public void setPortsScaled(boolean flag) {
boolean oldValue = portsScaled;
portsScaled = flag;
firePropertyChange(PORTS_SCALED_PROPERTY, oldValue, flag);
}
public boolean isPortsOnTop() {
return portsOnTop;
}
public void setPortsOnTop(boolean portsOnTop) {
this.portsOnTop = portsOnTop;
}
/**
* Returns true if the graph will be anti aliased.
*
* @return true if the graph is anti aliased
*/
public boolean isAntiAliased() {
return antiAliased;
}
/**
* Sets antialiasing on or off based on the boolean value.
* <p>
* Fires a property change for the ANTIALIASED_PROPERTY.
*
* @param newValue
* whether to turn antialiasing on or off
*/
public void setAntiAliased(boolean newValue) {
boolean oldValue = this.antiAliased;
this.antiAliased = newValue;
firePropertyChange(ANTIALIASED_PROPERTY, oldValue, newValue);
}
/**
* Returns true if the graph is editable (if it allows cells to be edited).
*
* @return true if the graph is editable
*/
public boolean isEditable() {
return editable;
}
/**
* Determines whether the graph is editable. Fires a property change event
* if the new setting is different from the existing setting.
* <p>
* Note: Editable determines whether the graph allows editing. This is not
* to be confused with enabled, which allows the graph to handle mouse
* events (including editing).
*
* @param flag
* a boolean value, true if the graph is editable
*/
public void setEditable(boolean flag) {
boolean oldValue = this.editable;
this.editable = flag;
firePropertyChange(EDITABLE_PROPERTY, oldValue, flag);
}
/**
* @return the groupsEditable
*/
public boolean isGroupsEditable() {
return groupsEditable;
}
/**
* @param groupsEditable the groupsEditable to set
*/
public void setGroupsEditable(boolean groupsEditable) {
this.groupsEditable = groupsEditable;
}
/**
* Returns true if the cell selection is enabled
*
* @return true if the cell selection is enabled
*/
public boolean isSelectionEnabled() {
return selectionEnabled;
}
/**
* Determines whether cell selection is enabled. Fires a property change
* event if the new setting is different from the existing setting.
*
* @param flag
* a boolean value, true if cell selection is enabled
*/
public void setSelectionEnabled(boolean flag) {
boolean oldValue = this.selectionEnabled;
this.selectionEnabled = flag;
firePropertyChange(SELECTIONENABLED_PROPERTY, oldValue, flag);
}
/**
* Returns true if graph allows invalid null ports during previews
*
* @return true if the graph allows invalid null ports during previews
*/
public boolean isPreviewInvalidNullPorts() {
return previewInvalidNullPorts;
}
/**
* Determines whether the graph allows invalid null ports during previews
*
* @param flag
* a boolean value, true if the graph allows invalid null ports
* during previews
*/
public void setPreviewInvalidNullPorts(boolean flag) {
this.previewInvalidNullPorts = flag;
}
/**
* Returns the current double buffering graphics object. Checks to see if
* the graph bounds has changed since the last time the off screen image was
* created and if so, creates a new image.
*
* @return the off screen graphics
*/
public Graphics getOffgraphics() {
if (!isDoubleBuffered()) {
// If double buffering is not enabled
return null;
}
// Get the bounds of the entire graph
Rectangle2D graphBounds = getBounds();
// Find the size of the double buffer in the JVM
int x = Math
.max(0, (int) graphBounds.getX());
int y = Math
.max(0, (int) graphBounds.getY());
int width = (int) graphBounds.getWidth();
int height = (int) graphBounds.getHeight();
boolean offScreenNeedsExtending = true;
Rectangle2D newOffscreenBuffer = new Rectangle2D.Double(0, 0, width, height);
if (offscreenBounds != null)
{
offScreenNeedsExtending = !(offscreenBounds
.contains(newOffscreenBuffer));
if (offScreenNeedsExtending)
{
width += offscreenBuffer;
height += offscreenBuffer;
newOffscreenBuffer = new Rectangle2D.Double(0, 0, width, height);
}
}
// Check whether the visible area is completely contained within the
// buffer. If not, the buffer need to be re-generated
if ((offscreen == null || offgraphics == null || offscreenBounds == null)
|| offScreenNeedsExtending) {
if (offscreen != null) {
offscreen.flush();
}
if (offgraphics != null) {
offgraphics.dispose();
}
offscreen = null;
offgraphics = null;
Runtime runtime = Runtime.getRuntime();
long maxMemory = runtime.maxMemory();
long allocatedMemory = runtime.totalMemory();
long freeMemory = runtime.freeMemory();
long totalFreeMemory = (freeMemory + (maxMemory - allocatedMemory)) / 1024;
// Calculate size of buffer required (assuming TYPE_INT_RGB which
// stores each pixel in a 32-bit int )
long memoryRequired = width*height*4/1024;
if (memoryRequired > totalFreeMemory) {
if (lastBufferAllocated) {
// If the last attempt to allocate a buffer worked it might
// be we need to reclaim the memory before the next one
// will work
System.gc();
}
lastBufferAllocated = false;
return null;
}
if (offscreen == null && volatileOffscreen) {
try {
offscreen = createVolatileImage(width, height);
} catch (OutOfMemoryError e) {
offscreen = null;
offgraphics = null;
}
}
if (offscreen == null) {
// Probably running in headless mode, try to create a buffered
// image.
createBufferedImage(width, height);
}
if (offscreen == null) {
// TODO assume the graph is too large and only buffer part
// of it, might also be faster to calculate in
// advance whether they is enough memory to create image
// rather than let it try and throw error.
lastBufferAllocated = false;
return null;
}
lastBufferAllocated = true;
setupOffScreen(x, y, width, height, newOffscreenBuffer);
} else if (offscreen instanceof VolatileImage) {
int valCode = ((VolatileImage) offscreen)
.validate(getGraphicsConfiguration());
if (!volatileOffscreen) {
offscreen.flush();
offgraphics.dispose();
offscreen = null;
offgraphics = null;
createBufferedImage(width,height);
setupOffScreen(x, y, width, height, newOffscreenBuffer);
} else if (valCode == VolatileImage.IMAGE_INCOMPATIBLE) {
offscreen.flush();
offgraphics.dispose();
try {
offscreen = createVolatileImage(width, height);
} catch (OutOfMemoryError e) {
offscreen = null;
offgraphics = null;
return null;
}
setupOffScreen(x, y, width, height, newOffscreenBuffer);
} else if (valCode == VolatileImage.IMAGE_RESTORED) {
addOffscreenDirty(new Rectangle2D.Double(0, 0, getWidth(), getHeight()));
}
}
Rectangle2D offscreenDirty = getOffscreenDirty();
if (offscreenDirty != null) {
if (isOpaque()) {
offgraphics.setColor(getBackground());
offgraphics.setPaintMode();
} else {
((Graphics2D) offgraphics).setComposite(AlphaComposite.getInstance(
AlphaComposite.CLEAR, 0.0f));
}
toScreen(offscreenDirty);
offscreenDirty.setRect(offscreenDirty.getX()
- (getHandleSize() + 1), offscreenDirty.getY()
- (getHandleSize() + 1), offscreenDirty.getWidth()
+ (getHandleSize() + 1) * 2, offscreenDirty.getHeight()
+ (getHandleSize() + 1) * 2);
offgraphics.fillRect((int) offscreenDirty.getX(),
(int) offscreenDirty.getY(), (int) offscreenDirty
.getWidth(), (int) offscreenDirty.getHeight());
if (!isOpaque()) {
((Graphics2D) offgraphics).setComposite(AlphaComposite.SrcOver);
}
((BasicGraphUI) getUI()).drawGraph(offgraphics, offscreenDirty);
clearOffscreenDirty();
}
return offgraphics;
}
/**
* Utility method to create a standard buffered image
* @param width
* @param height
*/
protected void createBufferedImage(int width, int height) {
GraphicsConfiguration graphicsConfig = getGraphicsConfiguration();
if (graphicsConfig != null) {
try {
offscreen = graphicsConfig.createCompatibleImage(width, height,
(isOpaque()) ? Transparency.OPAQUE
: Transparency.TRANSLUCENT);
} catch (OutOfMemoryError e) {
offscreen = null;
offgraphics = null;
} catch (NegativeArraySizeException e) {
// Customer reported this exception in DataBufferInt 13/12/2008
offscreen = null;
offgraphics = null;
// System.out.println("width = " + width);
// System.out.println("height = " + height);
}
} else {
try {
offscreen = new BufferedImage(width, height,
isOpaque() ? BufferedImage.TYPE_INT_RGB
: BufferedImage.TYPE_INT_ARGB);
} catch (OutOfMemoryError e) {
offscreen = null;
offgraphics = null;
}
}
}
/**
* Utility method that initialises the offscreen graphics area
* @param x
* @param y
* @param width
* @param height
* @param newOffscreenBuffer
*/
protected void setupOffScreen(int x, int y, int width, int height, Rectangle2D newOffscreenBuffer) {
offgraphics = offscreen.getGraphics();
if (isOpaque()) {
offgraphics.setColor(getBackground());
offgraphics.setPaintMode();
} else {
((Graphics2D) offgraphics).setComposite(AlphaComposite.getInstance(
AlphaComposite.CLEAR, 0.0f));
}
offgraphics.fillRect(0, 0, width, height);
if (!isOpaque()) {
((Graphics2D) offgraphics).setComposite(AlphaComposite.SrcOver);
}
((BasicGraphUI)getUI()).drawGraph(offgraphics, null);
offscreenBounds = newOffscreenBuffer;
offscreenOffset = new Point2D.Double(x, y);
// Clear the offscreen, we've just drawn the whole thing
clearOffscreenDirty();
}
/**
* @return the offscreen
*/
public Image getOffscreen() {
return offscreen;
}
/**
* Returns the area that is deemed dirty for the next double buffered redraw
*
* @return the area that is deemed dirty for the next double buffered redraw
*/
public Rectangle2D getOffscreenDirty() {
return offscreenDirty;
}
/**
* Adds the specified area to the region deemed dirty for the next double
* buffered redraw
*
* @param offscreenDirty
* the region to add
*/
public void addOffscreenDirty(Rectangle2D offscreenDirty) {
if (this.offscreenDirty == null && offscreenDirty != null) {
this.offscreenDirty = (Rectangle2D) offscreenDirty.clone();
} else if (offscreenDirty != null) {
this.offscreenDirty.add(offscreenDirty);
}
}
/**
* Clears the region deemed dirty for the next double buffered redraw
*/
public void clearOffscreenDirty() {
offscreenDirty = null;
}
/**
* Schedules the offscreen resources taken by the offscreen buffer to
* be reclaimed. Note that this does not force garbage collection
*/
public void releaseOffscreenResources() {
offscreen.flush();
offgraphics.dispose();
offscreen = null;
offgraphics = null;
}
/**
* Utility method to draw the off screen buffer
*
* @param dx1
* the <i>x</i> coordinate of the first corner of the
* destination rectangle.
* @param dy1
* the <i>y</i> coordinate of the first corner of the
* destination rectangle.
* @param dx2
* the <i>x</i> coordinate of the second corner of the
* destination rectangle.
* @param dy2
* the <i>y</i> coordinate of the second corner of the
* destination rectangle.
* @param sx1
* the <i>x</i> coordinate of the first corner of the source
* rectangle.
* @param sy1
* the <i>y</i> coordinate of the first corner of the source
* rectangle.
* @param sx2
* the <i>x</i> coordinate of the second corner of the source
* rectangle.
* @param sy2
* the <i>y</i> coordinate of the second corner of the source
* rectangle.
* @return <code>true</code> if the current output representation is
* complete; <code>false</code> otherwise.
*/
public boolean drawImage(int dx1, int dy1, int dx2, int dy2, int sx1,
int sy1, int sx2, int sy2) {
getOffgraphics();
return getGraphics().drawImage(offscreen, (int) sx1, (int) sy1,
(int) sx2, (int) sy2, (int) sx1, (int) sy1, (int) sx2,
(int) sy2, this);
}
public boolean drawImage(Graphics g) {
Rectangle rect = getBounds();
return getGraphics().drawImage(offscreen, rect.x, rect.y,
rect.x + rect.width, rect.y + rect.height, rect.x, rect.y,
rect.x + rect.width, rect.y + rect.height, this);
}
/**
* Returns the background image.
*
* @return Returns the backgroundImage.
*/
public ImageIcon getBackgroundImage() {
return backgroundImage;
}
/**
* Sets the background image. Fires a property change event for
* {@link #PROPERTY_BACKGROUNDIMAGE}.
*
* @param backgroundImage
* The backgroundImage to set.
*/
public void setBackgroundImage(ImageIcon backgroundImage) {
ImageIcon oldValue = this.backgroundImage;
this.backgroundImage = backgroundImage;
clearOffscreen();
firePropertyChange(PROPERTY_BACKGROUNDIMAGE, oldValue, backgroundImage);
}
/**
* Override parent to clear offscreen double buffer
*/
public void setBackground(Color bg) {
clearOffscreen();
super.setBackground(bg);
}
/**
* @return the backgroundScaled
*/
public boolean isBackgroundScaled() {
return backgroundScaled;
}
/**
* @return the offscreenOffset
*/
public Point2D getOffscreenOffset() {
return offscreenOffset;
}
/**
* @param offscreenOffset the offscreenOffset to set
*/
public void setOffscreenOffset(Point2D offscreenOffset) {
this.offscreenOffset = offscreenOffset;
}
/**
* @return the volatileOffscreen
*/
public boolean isVolatileOffscreen() {
return volatileOffscreen;
}
/**
* @param volatileOffscreen the volatileOffscreen to set
*/
public void setVolatileOffscreen(boolean volatileOffscreen) {
this.volatileOffscreen = volatileOffscreen;
}
/**
* @param backgroundScaled
* the backgroundScaled to set
*/
public void setBackgroundScaled(boolean backgroundScaled) {
this.backgroundScaled = backgroundScaled;
}
/**
* @return the backgroundComponent
*/
public Component getBackgroundComponent() {
return backgroundComponent;
}
/**
* @param backgroundComponent
* the backgroundComponent to set
*/
public void setBackgroundComponent(Component backgroundComponent) {
clearOffscreen();
this.backgroundComponent = backgroundComponent;
}
/*
* Overriden to change painting style for opaque components
* @see javax.swing.JComponent#setOpaque(boolean)
*/
public void setOpaque(boolean opaque) {
// Due to problems with XOR painting on transparent backgrounds
// switch off XOR for non-opaque components
if (!opaque) {
setXorEnabled(false);
}
super.setOpaque(opaque);
}
/**
* Returns the <code>GraphModel</code> that is providing the data.
*
* @return the model that is providing the data
*/
public GraphModel getModel() {
return graphModel;
}
/**
* Sets the <code>GraphModel</code> that will provide the data. Note:
* Updates the current GraphLayoutCache's model using setModel if the
* GraphLayoutCache points to a different model.
* <p>
* Fires a property change for the GRAPH_MODEL_PROPERTY.
*
* @param newModel
* the <code>GraphModel</code> that is to provide the data
*/
public void setModel(GraphModel newModel) {
GraphModel oldModel = graphModel;
graphModel = newModel;
clearOffscreen();
firePropertyChange(GRAPH_MODEL_PROPERTY, oldModel, graphModel);
// FIX: Use Listener
if (graphLayoutCache != null
&& graphLayoutCache.getModel() != graphModel)
graphLayoutCache.setModel(graphModel);
clearSelection();
invalidate();
}
/**
* Returns the <code>GraphLayoutCache</code> that is providing the
* view-data.
*
* @return the view that is providing the view-data
*/
public GraphLayoutCache getGraphLayoutCache() {
return graphLayoutCache;
}
/**
* Sets the <code>GraphLayoutCache</code> that will provide the view-data.
* <p>
* Note: Updates the graphs's model using using the model from the layout
* cache.
* <p>
* Fires a property change for the GRAPH_LAYOUT_CACHE_PROPERTY.
*
* @param newLayoutCache
* the <code>GraphLayoutCache</code> that is to provide the
* view-data
*/
public void setGraphLayoutCache(GraphLayoutCache newLayoutCache) {
if (!isSelectionEmpty())
{
clearSelection();
}
GraphLayoutCache oldLayoutCache = graphLayoutCache;
graphLayoutCache = newLayoutCache;
clearOffscreen();
firePropertyChange(GRAPH_LAYOUT_CACHE_PROPERTY, oldLayoutCache,
graphLayoutCache);
if (graphLayoutCache != null
&& graphLayoutCache.getModel() != getModel()) {
setModel(graphLayoutCache.getModel());
} else {
// Forces an update of the layout cache internal state (ports field)
graphLayoutCache.update();
}
invalidate();
}
/**
* Returns the <code>MarqueeHandler</code> that will handle marquee
* selection.
*/
public BasicMarqueeHandler getMarqueeHandler() {
return marquee;
}
/**
* Sets the <code>MarqueeHandler</code> that will handle marquee
* selection.
*
* @param newMarquee
* the <code>BasicMarqueeHandler</code> that is to provide
* marquee handling
*/
public void setMarqueeHandler(BasicMarqueeHandler newMarquee) {
BasicMarqueeHandler oldMarquee = marquee;
marquee = newMarquee;
firePropertyChange(MARQUEE_HANDLER_PROPERTY, oldMarquee, newMarquee);
invalidate();
}
/**
* Determines what happens when editing is interrupted by selecting another
* cell in the graph, a change in the graph's data, or by some other means.
* Setting this property to <code>true</code> causes the changes to be
* automatically saved when editing is interrupted.
* <p>
* Fires a property change for the INVOKES_STOP_CELL_EDITING_PROPERTY.
*
* @param newValue
* true means that <code>stopCellEditing</code> is invoked when
* editing is interruped, and data is saved; false means that
* <code>cancelCellEditing</code> is invoked, and changes are
* lost
*/
public void setInvokesStopCellEditing(boolean newValue) {
boolean oldValue = invokesStopCellEditing;
invokesStopCellEditing = newValue;
firePropertyChange(INVOKES_STOP_CELL_EDITING_PROPERTY, oldValue,
newValue);
}
/**
* Returns the indicator that tells what happens when editing is
* interrupted.
*
* @return the indicator that tells what happens when editing is interrupted
* @see #setInvokesStopCellEditing
*
*/
public boolean getInvokesStopCellEditing() {
return invokesStopCellEditing;
}
/**
* Returns <code>true</code> if the graph and the cell are editable. This
* is invoked from the UI before editing begins to ensure that the given
* cell can be edited.
*
* @return true if the specified cell is editable
* @see #isEditable
*
*/
public boolean isCellEditable(Object cell) {
if (cell != null) {
CellView view = graphLayoutCache.getMapping(cell, false);
if (view != null) {
return isEditable()
&& GraphConstants.isEditable(view.getAllAttributes());
}
}
return false;
}
/**
* Overrides <code>JComponent</code>'s<code>getToolTipText</code>
* method in order to allow the graph to create a tooltip for the topmost
* cell under the mousepointer. This differs from JTree where the renderers
* tooltip is used.
* <p>
* NOTE: For <code>JGraph</code> to properly display tooltips of its
* renderers, <code>JGraph</code> must be a registered component with the
* <code>ToolTipManager</code>. This can be done by invoking
* <code>ToolTipManager.sharedInstance().registerComponent(graph)</code>.
* This is not done automatically!
*
* @param e
* the <code>MouseEvent</code> that initiated the
* <code>ToolTip</code> display
* @return a string containing the tooltip or <code>null</code> if
* <code>event</code> is null
*/
public String getToolTipText(MouseEvent e) {
if (e != null) {
Object cell = getFirstCellForLocation(e.getX(), e.getY());
CellView view = getGraphLayoutCache().getMapping(cell, false);
if (view != null) {
Component c = view.getRendererComponent(this, false, false,
false);
if (c instanceof JComponent) {
Rectangle2D rect = getCellBounds(cell);
Point2D where = fromScreen(e.getPoint());
// Pass the event to the renderer in graph coordinates;
// the renderer is ignorant of screen scaling
e = new MouseEvent(c, e.getID(), e.getWhen(), e
.getModifiers(),
(int) (where.getX() - rect.getX()), (int) (where
.getY() - rect.getY()), e.getClickCount(),
e.isPopupTrigger());
return ((JComponent) c).getToolTipText(e);
}
}
}
return super.getToolTipText(e);
}
//
// The following are convenience methods that get forwarded to the
// current GraphSelectionModel.
//
/**
* Sets the graph's selection model. When a <code>null</code> value is
* specified an emtpy <code>selectionModel</code> is used, which does not
* allow selections.
*
* @param selectionModel
* the <code>GraphSelectionModel</code> to use, or
* <code>null</code> to disable selections
* @see GraphSelectionModel
*
*/
public void setSelectionModel(GraphSelectionModel selectionModel) {
if (selectionModel == null)
selectionModel = EmptySelectionModel.sharedInstance();
GraphSelectionModel oldValue = this.selectionModel;
// Remove Redirector From Old Selection Model
if (this.selectionModel != null && selectionRedirector != null)
this.selectionModel
.removeGraphSelectionListener(selectionRedirector);
this.selectionModel = selectionModel;
// Add Redirector To New Selection Model
if (selectionRedirector != null)
this.selectionModel.addGraphSelectionListener(selectionRedirector);
firePropertyChange(SELECTION_MODEL_PROPERTY, oldValue,
this.selectionModel);
}
/**
* Returns the model for selections. This should always return a non-
* <code>null</code> value. If you don't want to allow anything to be
* selected set the selection model to <code>null</code>, which forces an
* empty selection model to be used.
*
* @return the current selection model
* @see #setSelectionModel
*
*/
public GraphSelectionModel getSelectionModel() {
return selectionModel;
}
/**
* Clears the selection.
*/
public void clearSelection() {
getSelectionModel().clearSelection();
}
/**
* Returns true if the selection is currently empty.
*
* @return true if the selection is currently empty
*/
public boolean isSelectionEmpty() {
return getSelectionModel().isSelectionEmpty();
}
/**
* Adds a listener for <code>GraphSelection</code> events.
*
* @param tsl
* the <code>GraphSelectionListener</code> that will be
* notified when a cell is selected or deselected (a "negative
* selection")
*/
public void addGraphSelectionListener(GraphSelectionListener tsl) {
listenerList.add(GraphSelectionListener.class, tsl);
if (listenerList.getListenerCount(GraphSelectionListener.class) != 0
&& selectionRedirector == null) {
selectionRedirector = new GraphSelectionRedirector();
selectionModel.addGraphSelectionListener(selectionRedirector);
}
}
/**
* Removes a <code>GraphSelection</code> listener.
*
* @param tsl
* the <code>GraphSelectionListener</code> to remove
*/
public void removeGraphSelectionListener(GraphSelectionListener tsl) {
listenerList.remove(GraphSelectionListener.class, tsl);
if (listenerList.getListenerCount(GraphSelectionListener.class) == 0
&& selectionRedirector != null) {
selectionModel.removeGraphSelectionListener(selectionRedirector);
selectionRedirector = null;
}
}
/**
* Notifies all listeners that have registered interest for notification on
* this event type. The event instance is lazily created using the
* parameters passed into the fire method.
*
* @param e
* the <code>GraphSelectionEvent</code> generated by the
* <code>GraphSelectionModel</code> when a cell is selected or
* deselected
* @see javax.swing.event.EventListenerList
*
*/
protected void fireValueChanged(GraphSelectionEvent e) {
// Guaranteed to return a non-null array
Object[] listeners = listenerList.getListenerList();
// Process the listeners last to first, notifying
// those that are interested in this event
for (int i = listeners.length - 2; i >= 0; i -= 2) {
if (listeners[i] == GraphSelectionListener.class) {
((GraphSelectionListener) listeners[i + 1]).valueChanged(e);
}
}
}
/**
* Selects the specified cell.
*
* @param cell
* the <code>Object</code> specifying the cell to select
*/
public void setSelectionCell(Object cell) {
getSelectionModel().setSelectionCell(cell);
}
/**
* Selects the specified cells.
*
* @param cells
* an array of objects that specifies the cells to select
*/
public void setSelectionCells(Object[] cells) {
getSelectionModel().setSelectionCells(cells);
}
/**
* Adds the cell identified by the specified <code>Object</code> to the
* current selection.
*
* @param cell
* the cell to be added to the selection
*/
public void addSelectionCell(Object cell) {
getSelectionModel().addSelectionCell(cell);
}
/**
* Adds each cell in the array of cells to the current selection.
*
* @param cells
* an array of objects that specifies the cells to add
*/
public void addSelectionCells(Object[] cells) {
getSelectionModel().addSelectionCells(cells);
}
/**
* Removes the cell identified by the specified Object from the current
* selection.
*
* @param cell
* the cell to be removed from the selection
*/
public void removeSelectionCell(Object cell) {
getSelectionModel().removeSelectionCell(cell);
}
/**
* Returns the first selected cell.
*
* @return the <code>Object</code> for the first selected cell, or
* <code>null</code> if nothing is currently selected
*/
public Object getSelectionCell() {
return getSelectionModel().getSelectionCell();
}
/**
* Returns all selected cells.
*
* @return an array of objects representing the selected cells, or
* <code>null</code> if nothing is currently selected
*/
public Object[] getSelectionCells() {
return getSelectionModel().getSelectionCells();
}
/**
* Returns all selected cells in <code>cells</code>.
*/
public Object[] getSelectionCells(Object[] cells) {
if (cells != null) {
List selected = new ArrayList(cells.length);
for (int i = 0; i < cells.length; i++) {
if (isCellSelected(cells[i]))
selected.add(cells[i]);
}
return selected.toArray();
}
return null;
}
/**
* Returns the selection cell at the specified location.
*
* @return Returns the selection cell for <code>pt</code>.
*/
public Object getSelectionCellAt(Point2D pt) {
pt = fromScreen((Point2D) pt.clone());
Object[] cells = getSelectionCells();
if (cells != null) {
for (int i = 0; i < cells.length; i++)
if (getCellBounds(cells[i]).contains(pt.getX(), pt.getY()))
return cells[i];
}
return null;
}
/**
* Returns the number of cells selected.
*
* @return the number of cells selected
*/
public int getSelectionCount() {
return getSelectionModel().getSelectionCount();
}
/**
* Returns true if the cell is currently selected.
*
* @param cell
* an object identifying a cell
* @return true if the cell is selected
*/
public boolean isCellSelected(Object cell) {
return getSelectionModel().isCellSelected(cell);
}
/**
* Scrolls to the specified cell. Only works when this <code>JGraph</code>
* is contained in a <code>JScrollPane</code>.
*
* @param cell
* the object identifying the cell to bring into view
*/
public void scrollCellToVisible(Object cell) {
Rectangle2D bounds = getCellBounds(cell);
if (bounds != null) {
Rectangle2D b2 = toScreen((Rectangle2D) bounds.clone());
scrollRectToVisible(new Rectangle((int) b2.getX(), (int) b2.getY(),
(int) b2.getWidth(), (int) b2.getHeight()));
}
}
/**
* Makes sure the specified point is visible.
*
* @param p
* the point that should be visible
*/
public void scrollPointToVisible(Point2D p) {
if (p != null)
scrollRectToVisible(new Rectangle((int) p.getX(), (int) p.getY(),
1, 1));
}
/**
* Returns true if the graph is being edited. The item that is being edited
* can be obtained using <code>getEditingCell</code>.
*
* @return true if the user is currently editing a cell
* @see #getSelectionCell
*
*/
public boolean isEditing() {
GraphUI graph = getUI();
if (graph != null)
return graph.isEditing(this);
return false;
}
/**
* Ends the current editing session. (The
* <code>DefaultGraphCellEditor</code> object saves any edits that are
* currently in progress on a cell. Other implementations may operate
* differently.) Has no effect if the tree isn't being edited. <blockquote>
* <b>Note: </b> <br>
* To make edit-saves automatic whenever the user changes their position in
* the graph, use {@link #setInvokesStopCellEditing}. </blockquote>
*
* @return true if editing was in progress and is now stopped, false if
* editing was not in progress
*/
public boolean stopEditing() {
GraphUI graph = getUI();
if (graph != null)
return graph.stopEditing(this);
return false;
}
/**
* Cancels the current editing session. Has no effect if the graph isn't
* being edited.
*/
public void cancelEditing() {
GraphUI graph = getUI();
if (graph != null)
graph.cancelEditing(this);
}
/**
* Selects the specified cell and initiates editing. The edit-attempt fails
* if the <code>CellEditor</code> does not allow editing for the specified
* item.
*/
public void startEditingAtCell(Object cell) {
GraphUI graph = getUI();
if (graph != null)
graph.startEditingAtCell(this, cell);
}
/**
* Returns the cell that is currently being edited.
*
* @return the cell being edited
*/
public Object getEditingCell() {
GraphUI graph = getUI();
if (graph != null)
return graph.getEditingCell(this);
return null;
}
/**
* Messaged when the graph has changed enough that we need to resize the
* bounds, but not enough that we need to remove the cells (e.g cells were
* inserted into the graph). You should never have to invoke this, the UI
* will invoke this as it needs to. (Note: This is invoked by GraphUI, eg.
* after moving.)
*/
public void graphDidChange() {
revalidate();
repaint();
}
/**
* Repaints the entire graph, regardless of what is marked dirty
*/
public void refresh() {
clearOffscreen();
repaint();
}
// /* (non-Javadoc)
// * @see javax.swing.JComponent#isOptimizedDrawingEnabled()
// */
// @Override
// public boolean isOptimizedDrawingEnabled() {
// return true;
// }
/**
* Returns a {@link BufferedImage} for the graph using inset as an empty
* border around the cells of the graph. If bg is null then a transparent
* background is applied to the image, else the background is filled with
* the bg color. Therefore, one should only use a null background if the
* fileformat support transparency, eg. GIF and PNG. For JPG, you can use
* <code>Color.WHITE</code> for example.
*
* @return Returns an image of the graph.
*/
public BufferedImage getImage(Color bg, int inset) {
// TODO, this method could just use the offscreen if available
Object[] cells = getRoots();
Rectangle2D bounds = getCellBounds(cells);
if (bounds != null) {
toScreen(bounds);
GraphicsConfiguration graphicsConfig = getGraphicsConfiguration();
BufferedImage img = null;
if (graphicsConfig != null) {
img = getGraphicsConfiguration().createCompatibleImage(
(int) bounds.getWidth() + 2 * inset,
(int) bounds.getHeight() + 2 * inset,
(bg != null) ? Transparency.OPAQUE
: Transparency.BITMASK);
} else {
img = new BufferedImage((int) bounds.getWidth() + 2 * inset,
(int) bounds.getHeight() + 2 * inset,
(bg != null) ? BufferedImage.TYPE_INT_RGB
: BufferedImage.TYPE_INT_ARGB);
}
Graphics2D graphics = img.createGraphics();
if (bg != null) {
graphics.setColor(bg);
graphics.fillRect(0, 0, img.getWidth(), img.getHeight());
} else {
graphics.setComposite(AlphaComposite.getInstance(
AlphaComposite.CLEAR, 0.0f));
graphics.fillRect(0, 0, img.getWidth(), img.getHeight());
graphics.setComposite(AlphaComposite.SrcOver);
}
graphics.translate((int) (-bounds.getX() + inset), (int) (-bounds
.getY() + inset));
print(graphics);
graphics.dispose();
return img;
}
return null;
}
/**
* Calculates the clip
* @param change
* @return the total region dirty as a result of this change
*/
public Rectangle2D getClipRectangle(GraphLayoutCacheChange change) {
List removed = DefaultGraphModel.getDescendants(getModel(), change.getRemoved());
Rectangle2D removedBounds = (removed != null && !removed.isEmpty()) ? getCellBounds(removed.toArray()) : null;
List inserted = DefaultGraphModel.getDescendants(getModel(), change.getInserted());
Rectangle2D insertedBounds = (inserted != null && !inserted.isEmpty()) ? getCellBounds(inserted.toArray()) : null;
List changed = DefaultGraphModel.getDescendants(getModel(), change.getChanged());
Rectangle2D changedBounds = (changed != null && !changed.isEmpty()) ? getCellBounds(changed.toArray()) : null;
List context = DefaultGraphModel.getDescendants(getModel(), change.getContext());
Rectangle2D contextBounds = (context != null && !context.isEmpty()) ? getCellBounds(context.toArray()) : null;
Rectangle2D clip = removedBounds;
if (clip == null) {
clip = insertedBounds;
} else if (insertedBounds != null) {
clip.add(insertedBounds);
}
if (clip == null) {
clip = changedBounds;
} else if (changedBounds != null) {
clip.add(changedBounds);
}
if (clip == null) {
clip = contextBounds;
} else if (contextBounds != null) {
clip.add(contextBounds);
}
return clip;
}
/**
* Serialization support.
*/
private void writeObject(ObjectOutputStream s) throws IOException {
Vector values = new Vector();
s.defaultWriteObject();
// Save the cellEditor, if its Serializable.
if (graphModel instanceof Serializable) {
values.addElement("graphModel");
values.addElement(graphModel);
}
// Save the graphModel, if its Serializable.
values.addElement("graphLayoutCache");
values.addElement(graphLayoutCache);
// Save the selectionModel, if its Serializable.
if (selectionModel instanceof Serializable) {
values.addElement("selectionModel");
values.addElement(selectionModel);
}
// Save the marquee handler, if its Serializable.
if (marquee instanceof Serializable) {
values.addElement("marquee");
values.addElement(marquee);
}
s.writeObject(values);
if (getUIClassID().equals(uiClassID)) {
/*
* byte count = JComponent.getWriteObjCounter(this);
* JComponent.setWriteObjCounter(this, --count);
*/
if (/* count == 0 && */
ui != null) {
ui.installUI(this);
}
}
}
/**
* Serialization support.
*/
private void readObject(ObjectInputStream s) throws IOException,
ClassNotFoundException {
s.defaultReadObject();
Vector values = (Vector) s.readObject();
int indexCounter = 0;
int maxCounter = values.size();
if (indexCounter < maxCounter
&& values.elementAt(indexCounter).equals("graphModel")) {
graphModel = (GraphModel) values.elementAt(++indexCounter);
indexCounter++;
}
if (indexCounter < maxCounter
&& values.elementAt(indexCounter).equals("graphLayoutCache")) {
graphLayoutCache = (GraphLayoutCache) values
.elementAt(++indexCounter);
indexCounter++;
}
if (indexCounter < maxCounter
&& values.elementAt(indexCounter).equals("selectionModel")) {
selectionModel = (GraphSelectionModel) values
.elementAt(++indexCounter);
indexCounter++;
}
if (indexCounter < maxCounter
&& values.elementAt(indexCounter).equals("marquee")) {
marquee = (BasicMarqueeHandler) values.elementAt(++indexCounter);
indexCounter++;
}
// Reinstall the redirector.
if (listenerList.getListenerCount(GraphSelectionListener.class) != 0) {
selectionRedirector = new GraphSelectionRedirector();
selectionModel.addGraphSelectionListener(selectionRedirector);
}
}
/**
* <code>EmptySelectionModel</code> is a <code>GraphSelectionModel</code>
* that does not allow anything to be selected.
* <p>
* <strong>Warning: </strong> Serialized objects of this class will not be
* compatible with future Swing releases. The current serialization support
* is appropriate for short term storage or RMI between applications running
* the same version of Swing. A future release of Swing will provide support
* for long term persistence.
*/
public static class EmptySelectionModel extends DefaultGraphSelectionModel {
/** Unique shared instance. */
protected static final EmptySelectionModel sharedInstance = new EmptySelectionModel();
/**
* A <code>null</code> implementation that constructs an
* EmptySelectionModel.
*/
public EmptySelectionModel() {
super(null);
}
/** Returns a shared instance of an empty selection model. */
static public EmptySelectionModel sharedInstance() {
return sharedInstance;
}
/** A <code>null</code> implementation that selects nothing. */
public void setSelectionCells(Object[] cells) {
}
/** A <code>null</code> implementation that adds nothing. */
public void addSelectionCells(Object[] cells) {
}
/** A <code>null</code> implementation that removes nothing. */
public void removeSelectionCells(Object[] cells) {
}
}
/**
* Handles creating a new <code>GraphSelectionEvent</code> with the
* <code>JGraph</code> as the source and passing it off to all the
* listeners.
* <p>
* <strong>Warning: </strong> Serialized objects of this class will not be
* compatible with future Swing releases. The current serialization support
* is appropriate for short term storage or RMI between applications running
* the same version of Swing. A future release of Swing will provide support
* for long term persistence.
*/
protected class GraphSelectionRedirector implements Serializable,
GraphSelectionListener {
/**
* Invoked by the <code>GraphSelectionModel</code> when the selection
* changes.
*
* @param e
* the <code>GraphSelectionEvent</code> generated by the
* <code>GraphSelectionModel</code>
*/
public void valueChanged(GraphSelectionEvent e) {
GraphSelectionEvent newE;
newE = (GraphSelectionEvent) e.cloneWithSource(JGraph.this);
fireValueChanged(newE);
}
} // End of class JGraph.GraphSelectionRedirector
//
// Scrollable interface
//
/**
* Returns the preferred display size of a <code>JGraph</code>. The
* height is determined from <code>getPreferredWidth</code>.
*
* @return the graph's preferred size
*/
public Dimension getPreferredScrollableViewportSize() {
return getPreferredSize();
}
/**
* Returns the amount to increment when scrolling. The amount is 4.
*
* @param visibleRect
* the view area visible within the viewport
* @param orientation
* either <code>SwingConstants.VERTICAL</code> or
* <code>SwingConstants.HORIZONTAL</code>
* @param direction
* less than zero to scroll up/left, greater than zero for
* down/right
* @return the "unit" increment for scrolling in the specified direction
* @see javax.swing.JScrollBar#setUnitIncrement(int)
*
*/
public int getScrollableUnitIncrement(Rectangle visibleRect,
int orientation, int direction) {
if (orientation == SwingConstants.VERTICAL) {
return 2;
}
return 4;
}
/**
* Returns the amount for a block increment, which is the height or width of
* <code>visibleRect</code>, based on <code>orientation</code>.
*
* @param visibleRect
* the view area visible within the viewport
* @param orientation
* either <code>SwingConstants.VERTICAL</code> or
* <code>SwingConstants.HORIZONTAL</code>
* @param direction
* less than zero to scroll up/left, greater than zero for
* down/right.
* @return the "block" increment for scrolling in the specified direction
* @see javax.swing.JScrollBar#setBlockIncrement(int)
*
*/
public int getScrollableBlockIncrement(Rectangle visibleRect,
int orientation, int direction) {
return (orientation == SwingConstants.VERTICAL) ? visibleRect.height
: visibleRect.width;
}
/**
* Returns false to indicate that the width of the viewport does not
* determine the width of the graph, unless the preferred width of the graph
* is smaller than the viewports width. In other words: ensure that the
* graph is never smaller than its viewport.
*
* @return false
* @see Scrollable#getScrollableTracksViewportWidth
*
*/
public boolean getScrollableTracksViewportWidth() {
if (getParent() instanceof JViewport) {
return (((JViewport) getParent()).getWidth() > getPreferredSize().width);
}
return false;
}
/**
* Returns false to indicate that the height of the viewport does not
* determine the height of the graph, unless the preferred height of the
* graph is smaller than the viewports height. In other words: ensure that
* the graph is never smaller than its viewport.
*
* @return false
* @see Scrollable#getScrollableTracksViewportHeight
*
*/
public boolean getScrollableTracksViewportHeight() {
if (getParent() instanceof JViewport) {
return (((JViewport) getParent()).getHeight() > getPreferredSize().height);
}
return false;
}
/**
* Returns a string representation of this <code>JGraph</code>. This
* method is intended to be used only for debugging purposes, and the
* content and format of the returned string may vary between
* implementations. The returned string may be empty but may not be
* <code>null</code>.
*
* @return a string representation of this <code>JGraph</code>.
*/
protected String paramString() {
String editableString = (editable ? "true" : "false");
String invokesStopCellEditingString = (invokesStopCellEditing ? "true"
: "false");
return super.paramString() + ",editable=" + editableString
+ ",invokesStopCellEditing=" + invokesStopCellEditingString;
}
public static void main(String[] args) {
System.out.println(VERSION);
}
}