/** * $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 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); } } } }