/**
* $Id: mxConnectionHandler.java,v 1.1 2012/11/15 13:26:44 gaudenz Exp $
* Copyright (c) 2008, Gaudenz Alder
*/
package com.mxgraph.swing.handler;
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.*;
import com.mxgraph.util.mxEventSource.mxIEventListener;
import com.mxgraph.view.mxCellState;
import com.mxgraph.view.mxGraph;
import com.mxgraph.view.mxGraphView;
import javax.swing.*;
import java.awt.*;
import java.awt.event.MouseEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
/**
* 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.
* <p/>
* 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);
}
}
}
}