/**
* Copyright (c) 2008, Gaudenz Alder
*/
package com.mxgraph.swing.handler;
import java.awt.Color;
import java.awt.Cursor;
import java.awt.Graphics;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.MouseEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import javax.swing.ImageIcon;
import javax.swing.JOptionPane;
import com.mxgraph.model.mxGeometry;
import com.mxgraph.model.mxIGraphModel;
import com.mxgraph.swing.mxGraphComponent;
import com.mxgraph.swing.mxGraphComponent.mxGraphControl;
import com.mxgraph.swing.util.mxMouseAdapter;
import com.mxgraph.util.mxConstants;
import com.mxgraph.util.mxEvent;
import com.mxgraph.util.mxEventObject;
import com.mxgraph.util.mxEventSource;
import com.mxgraph.util.mxEventSource.mxIEventListener;
import com.mxgraph.util.mxPoint;
import com.mxgraph.util.mxRectangle;
import com.mxgraph.view.mxCellState;
import com.mxgraph.view.mxGraph;
import com.mxgraph.view.mxGraphView;
/**
* Connection handler creates new connections between cells. This control is used to display the connector
* icon, while the preview is used to draw the line.
*
* mxEvent.CONNECT fires between begin- and endUpdate in mouseReleased. The <code>cell</code>
* property contains the inserted edge, the <code>event</code> and <code>target</code>
* properties contain the respective arguments that were passed to mouseReleased.
*/
public class mxConnectionHandler extends mxMouseAdapter
{
/**
*
*/
private static final long serialVersionUID = -2543899557644889853L;
/**
*
*/
public static Cursor CONNECT_CURSOR = new Cursor(Cursor.HAND_CURSOR);
/**
*
*/
protected mxGraphComponent graphComponent;
/**
* Holds the event source.
*/
protected mxEventSource eventSource = new mxEventSource(this);
/**
*
*/
protected mxConnectPreview connectPreview;
/**
* Specifies the icon to be used for creating new connections. If this is
* specified then it is used instead of the handle. Default is null.
*/
protected ImageIcon connectIcon = null;
/**
* Specifies the size of the handle to be used for creating new
* connections. Default is mxConstants.CONNECT_HANDLE_SIZE.
*/
protected int handleSize = mxConstants.CONNECT_HANDLE_SIZE;
/**
* Specifies if a handle should be used for creating new connections. This
* is only used if no connectIcon is specified. If this is false, then the
* source cell will be highlighted when the mouse is over the hotspot given
* in the marker. Default is mxConstants.CONNECT_HANDLE_ENABLED.
*/
protected boolean handleEnabled = mxConstants.CONNECT_HANDLE_ENABLED;
/**
*
*/
protected boolean select = true;
/**
* Specifies if the source should be cloned and used as a target if no
* target was selected. Default is false.
*/
protected boolean createTarget = false;
/**
* Appearance and event handling order wrt subhandles.
*/
protected boolean keepOnTop = true;
/**
*
*/
protected boolean enabled = true;
/**
*
*/
protected transient Point first;
/**
*
*/
protected transient boolean active = false;
/**
*
*/
protected transient Rectangle bounds;
/**
*
*/
protected transient mxCellState source;
/**
*
*/
protected transient mxCellMarker marker;
/**
*
*/
protected transient String error;
/**
*
*/
protected transient mxIEventListener resetHandler = new mxIEventListener()
{
public void invoke(Object source, mxEventObject evt)
{
reset();
}
};
/**
*
* @param graphComponent
*/
public mxConnectionHandler(mxGraphComponent graphComponent)
{
this.graphComponent = graphComponent;
// Installs the paint handler
graphComponent.addListener(mxEvent.AFTER_PAINT, new mxIEventListener()
{
public void invoke(Object sender, mxEventObject evt)
{
Graphics g = (Graphics) evt.getProperty("g");
paint(g);
}
});
connectPreview = createConnectPreview();
mxGraphControl graphControl = graphComponent.getGraphControl();
graphControl.addMouseListener(this);
graphControl.addMouseMotionListener(this);
// Installs the graph listeners and keeps them in sync
addGraphListeners(graphComponent.getGraph());
graphComponent.addPropertyChangeListener(new PropertyChangeListener()
{
public void propertyChange(PropertyChangeEvent evt)
{
if (evt.getPropertyName().equals("graph"))
{
removeGraphListeners((mxGraph) evt.getOldValue());
addGraphListeners((mxGraph) evt.getNewValue());
}
}
});
marker = new mxCellMarker(graphComponent)
{
/**
*
*/
private static final long serialVersionUID = 103433247310526381L;
// Overrides to return cell at location only if valid (so that
// there is no highlight for invalid cells that have no error
// message when the mouse is released)
protected Object getCell(MouseEvent e)
{
Object cell = super.getCell(e);
if (isConnecting())
{
if (source != null)
{
error = validateConnection(source.getCell(), cell);
if (error != null && error.length() == 0)
{
cell = null;
// Enables create target inside groups
if (createTarget)
{
error = null;
}
}
}
}
else if (!isValidSource(cell))
{
cell = null;
}
return cell;
}
// Sets the highlight color according to isValidConnection
protected boolean isValidState(mxCellState state)
{
if (isConnecting())
{
return error == null;
}
else
{
return super.isValidState(state);
}
}
// Overrides to use marker color only in highlight mode or for
// target selection
protected Color getMarkerColor(MouseEvent e, mxCellState state,
boolean isValid)
{
return (isHighlighting() || isConnecting()) ? super
.getMarkerColor(e, state, isValid) : null;
}
// Overrides to use hotspot only for source selection otherwise
// intersects always returns true when over a cell
protected boolean intersects(mxCellState state, MouseEvent e)
{
if (!isHighlighting() || isConnecting())
{
return true;
}
return super.intersects(state, e);
}
};
marker.setHotspotEnabled(true);
}
/**
* Installs the listeners to update the handles after any changes.
*/
protected void addGraphListeners(mxGraph graph)
{
// LATER: Install change listener for graph model, view
if (graph != null)
{
mxGraphView view = graph.getView();
view.addListener(mxEvent.SCALE, resetHandler);
view.addListener(mxEvent.TRANSLATE, resetHandler);
view.addListener(mxEvent.SCALE_AND_TRANSLATE, resetHandler);
graph.getModel().addListener(mxEvent.CHANGE, resetHandler);
}
}
/**
* Removes all installed listeners.
*/
protected void removeGraphListeners(mxGraph graph)
{
if (graph != null)
{
mxGraphView view = graph.getView();
view.removeListener(resetHandler, mxEvent.SCALE);
view.removeListener(resetHandler, mxEvent.TRANSLATE);
view.removeListener(resetHandler, mxEvent.SCALE_AND_TRANSLATE);
graph.getModel().removeListener(resetHandler, mxEvent.CHANGE);
}
}
/**
*
*/
protected mxConnectPreview createConnectPreview()
{
return new mxConnectPreview(graphComponent);
}
/**
*
*/
public mxConnectPreview getConnectPreview()
{
return connectPreview;
}
/**
*
*/
public void setConnectPreview(mxConnectPreview value)
{
connectPreview = value;
}
/**
* Returns true if the source terminal has been clicked and a new
* connection is currently being previewed.
*/
public boolean isConnecting()
{
return connectPreview.isActive();
}
/**
*
*/
public boolean isActive()
{
return active;
}
/**
* Returns true if no connectIcon is specified and handleEnabled is false.
*/
public boolean isHighlighting()
{
return connectIcon == null && !handleEnabled;
}
/**
*
*/
public boolean isEnabled()
{
return enabled;
}
/**
*
*/
public void setEnabled(boolean value)
{
enabled = value;
}
/**
*
*/
public boolean isKeepOnTop()
{
return keepOnTop;
}
/**
*
*/
public void setKeepOnTop(boolean value)
{
keepOnTop = value;
}
/**
*
*/
public void setConnectIcon(ImageIcon value)
{
connectIcon = value;
}
/**
*
*/
public ImageIcon getConnecIcon()
{
return connectIcon;
}
/**
*
*/
public void setHandleEnabled(boolean value)
{
handleEnabled = value;
}
/**
*
*/
public boolean isHandleEnabled()
{
return handleEnabled;
}
/**
*
*/
public void setHandleSize(int value)
{
handleSize = value;
}
/**
*
*/
public int getHandleSize()
{
return handleSize;
}
/**
*
*/
public mxCellMarker getMarker()
{
return marker;
}
/**
*
*/
public void setMarker(mxCellMarker value)
{
marker = value;
}
/**
*
*/
public void setCreateTarget(boolean value)
{
createTarget = value;
}
/**
*
*/
public boolean isCreateTarget()
{
return createTarget;
}
/**
*
*/
public void setSelect(boolean value)
{
select = value;
}
/**
*
*/
public boolean isSelect()
{
return select;
}
/**
*
*/
public void reset()
{
connectPreview.stop(false);
setBounds(null);
marker.reset();
active = false;
source = null;
first = null;
error = null;
}
/**
*
*/
public Object createTargetVertex(MouseEvent e, Object source)
{
mxGraph graph = graphComponent.getGraph();
Object clone = graph.cloneCells(new Object[] { source })[0];
mxIGraphModel model = graph.getModel();
mxGeometry geo = model.getGeometry(clone);
if (geo != null)
{
mxPoint point = graphComponent.getPointForEvent(e);
geo.setX(graph.snap(point.getX() - geo.getWidth() / 2));
geo.setY(graph.snap(point.getY() - geo.getHeight() / 2));
}
return clone;
}
/**
*
*/
public boolean isValidSource(Object cell)
{
return graphComponent.getGraph().isValidSource(cell);
}
/**
* Returns true. The call to mxGraph.isValidTarget is implicit by calling
* mxGraph.getEdgeValidationError in validateConnection. This is an
* additional hook for disabling certain targets in this specific handler.
*/
public boolean isValidTarget(Object cell)
{
return true;
}
/**
* Returns the error message or an empty string if the connection for the
* given source target pair is not valid. Otherwise it returns null.
*/
public String validateConnection(Object source, Object target)
{
if (target == null && createTarget)
{
return null;
}
if (!isValidTarget(target))
{
return "";
}
return graphComponent.getGraph().getEdgeValidationError(
connectPreview.getPreviewState().getCell(), source, target);
}
/**
*
*/
public void mousePressed(MouseEvent e)
{
if (!graphComponent.isForceMarqueeEvent(e)
&& !graphComponent.isPanningEvent(e)
&& !e.isPopupTrigger()
&& graphComponent.isEnabled()
&& isEnabled()
&& !e.isConsumed()
&& ((isHighlighting() && marker.hasValidState()) || (!isHighlighting()
&& bounds != null && bounds.contains(e.getPoint()))))
{
start(e, marker.getValidState());
e.consume();
}
}
/**
*
*/
public void start(MouseEvent e, mxCellState state)
{
first = e.getPoint();
connectPreview.start(e, state, "");
}
/**
*
*/
public void mouseMoved(MouseEvent e)
{
mouseDragged(e);
if (isHighlighting() && !marker.hasValidState())
{
source = null;
}
if (!isHighlighting() && source != null)
{
int imgWidth = handleSize;
int imgHeight = handleSize;
if (connectIcon != null)
{
imgWidth = connectIcon.getIconWidth();
imgHeight = connectIcon.getIconHeight();
}
int x = (int) source.getCenterX() - imgWidth / 2;
int y = (int) source.getCenterY() - imgHeight / 2;
if (graphComponent.getGraph().isSwimlane(source.getCell()))
{
mxRectangle size = graphComponent.getGraph().getStartSize(
source.getCell());
if (size.getWidth() > 0)
{
x = (int) (source.getX() + size.getWidth() / 2 - imgWidth / 2);
}
else
{
y = (int) (source.getY() + size.getHeight() / 2 - imgHeight / 2);
}
}
setBounds(new Rectangle(x, y, imgWidth, imgHeight));
}
else
{
setBounds(null);
}
if (source != null && (bounds == null || bounds.contains(e.getPoint())))
{
graphComponent.getGraphControl().setCursor(CONNECT_CURSOR);
e.consume();
}
}
/**
*
*/
public void mouseDragged(MouseEvent e)
{
if (!e.isConsumed() && graphComponent.isEnabled() && isEnabled())
{
// Activates the handler
if (!active && first != null)
{
double dx = Math.abs(first.getX() - e.getX());
double dy = Math.abs(first.getY() - e.getY());
int tol = graphComponent.getTolerance();
if (dx > tol || dy > tol)
{
active = true;
}
}
if (e.getButton() == 0 || (isActive() && connectPreview.isActive()))
{
mxCellState state = marker.process(e);
if (connectPreview.isActive())
{
connectPreview.update(e, marker.getValidState(), e.getX(),
e.getY());
setBounds(null);
e.consume();
}
else
{
source = state;
}
}
}
}
/**
*
*/
public void mouseReleased(MouseEvent e)
{
if (isActive())
{
if (error != null)
{
if (error.length() > 0)
{
JOptionPane.showMessageDialog(graphComponent, error);
}
}
else if (first != null)
{
mxGraph graph = graphComponent.getGraph();
double dx = first.getX() - e.getX();
double dy = first.getY() - e.getY();
if (connectPreview.isActive()
&& (marker.hasValidState() || isCreateTarget() || graph
.isAllowDanglingEdges()))
{
graph.getModel().beginUpdate();
try
{
Object dropTarget = null;
if (!marker.hasValidState() && isCreateTarget())
{
Object vertex = createTargetVertex(e, source.getCell());
dropTarget = graph.getDropTarget(
new Object[] { vertex }, e.getPoint(),
graphComponent.getCellAt(e.getX(), e.getY()));
if (vertex != null)
{
// Disables edges as drop targets if the target cell was created
if (dropTarget == null
|| !graph.getModel().isEdge(dropTarget))
{
mxCellState pstate = graph.getView().getState(
dropTarget);
if (pstate != null)
{
mxGeometry geo = graph.getModel()
.getGeometry(vertex);
mxPoint origin = pstate.getOrigin();
geo.setX(geo.getX() - origin.getX());
geo.setY(geo.getY() - origin.getY());
}
}
else
{
dropTarget = graph.getDefaultParent();
}
graph.addCells(new Object[] { vertex }, dropTarget);
}
// FIXME: Here we pre-create the state for the vertex to be
// inserted in order to invoke update in the connectPreview.
// This means we have a cell state which should be created
// after the model.update, so this should be fixed.
mxCellState targetState = graph.getView().getState(
vertex, true);
connectPreview.update(e, targetState, e.getX(),
e.getY());
}
Object cell = connectPreview.stop(
graphComponent.isSignificant(dx, dy), e);
if (cell != null)
{
graphComponent.getGraph().setSelectionCell(cell);
eventSource.fireEvent(new mxEventObject(
mxEvent.CONNECT, "cell", cell, "event", e,
"target", dropTarget));
}
e.consume();
}
finally
{
graph.getModel().endUpdate();
}
}
}
}
reset();
}
/**
*
*/
public void setBounds(Rectangle value)
{
if ((bounds == null && value != null)
|| (bounds != null && value == null)
|| (bounds != null && value != null && !bounds.equals(value)))
{
Rectangle tmp = bounds;
if (tmp != null)
{
if (value != null)
{
tmp.add(value);
}
}
else
{
tmp = value;
}
bounds = value;
if (tmp != null)
{
graphComponent.getGraphControl().repaint(tmp);
}
}
}
/**
* Adds the given event listener.
*/
public void addListener(String eventName, mxIEventListener listener)
{
eventSource.addListener(eventName, listener);
}
/**
* Removes the given event listener.
*/
public void removeListener(mxIEventListener listener)
{
eventSource.removeListener(listener);
}
/**
* Removes the given event listener for the specified event name.
*/
public void removeListener(mxIEventListener listener, String eventName)
{
eventSource.removeListener(listener, eventName);
}
/**
*
*/
public void paint(Graphics g)
{
if (bounds != null)
{
if (connectIcon != null)
{
g.drawImage(connectIcon.getImage(), bounds.x, bounds.y,
bounds.width, bounds.height, null);
}
else if (handleEnabled)
{
g.setColor(Color.BLACK);
g.draw3DRect(bounds.x, bounds.y, bounds.width - 1,
bounds.height - 1, true);
g.setColor(Color.GREEN);
g.fill3DRect(bounds.x + 1, bounds.y + 1, bounds.width - 2,
bounds.height - 2, true);
g.setColor(Color.BLUE);
g.drawRect(bounds.x + bounds.width / 2 - 1, bounds.y
+ bounds.height / 2 - 1, 1, 1);
}
}
}
}