/**
* $Id: mxEdgeHandler.java,v 1.1 2012/11/15 13:26:44 gaudenz Exp $
* Copyright (c) 2008-2012, JGraph Ltd
*/
package com.mxgraph.swing.handler;
import com.mxgraph.model.mxGeometry;
import com.mxgraph.model.mxIGraphModel;
import com.mxgraph.swing.mxGraphComponent;
import com.mxgraph.swing.util.mxSwingConstants;
import com.mxgraph.util.mxConstants;
import com.mxgraph.util.mxPoint;
import com.mxgraph.view.mxCellState;
import com.mxgraph.view.mxConnectionConstraint;
import com.mxgraph.view.mxGraph;
import com.mxgraph.view.mxGraphView;
import javax.swing.*;
import java.awt.*;
import java.awt.event.MouseEvent;
import java.awt.geom.Line2D;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
*
*/
public class mxEdgeHandler extends mxCellHandler {
/**
*
*/
protected boolean cloneEnabled = true;
/**
*
*/
protected Point[] p;
/**
*
*/
protected transient String error;
/**
* Workaround for alt-key-state not correct in mouseReleased.
*/
protected transient boolean gridEnabledEvent = false;
/**
* Workaround for shift-key-state not correct in mouseReleased.
*/
protected transient boolean constrainedEvent = false;
/**
*
*/
protected mxCellMarker marker = new mxCellMarker(graphComponent) {
/**
*
*/
private static final long serialVersionUID = 8826073441093831764L;
// Only returns edges if they are connectable and never returns
// the edge that is currently being modified
protected Object getCell(MouseEvent e) {
mxGraph graph = graphComponent.getGraph();
mxIGraphModel model = graph.getModel();
Object cell = super.getCell(e);
if (cell == mxEdgeHandler.this.state.getCell() || (!graph.isConnectableEdges() && model.isEdge(cell))) {
cell = null;
}
return cell;
}
// Sets the highlight color according to isValidConnection
protected boolean isValidState(mxCellState state) {
mxGraphView view = graphComponent.getGraph().getView();
mxIGraphModel model = graphComponent.getGraph().getModel();
Object edge = mxEdgeHandler.this.state.getCell();
boolean isSource = isSource(index);
mxCellState other = view.getTerminalPort(state, view.getState(model.getTerminal(edge, !isSource)), !isSource);
Object otherCell = (other != null) ? other.getCell() : null;
Object source = (isSource) ? state.getCell() : otherCell;
Object target = (isSource) ? otherCell : state.getCell();
error = validateConnection(source, target);
return error == null;
}
};
/**
* @param graphComponent
* @param state
*/
public mxEdgeHandler(mxGraphComponent graphComponent, mxCellState state) {
super(graphComponent, state);
}
/**
*
*/
public void setCloneEnabled(boolean cloneEnabled) {
this.cloneEnabled = cloneEnabled;
}
/**
*
*/
public boolean isCloneEnabled() {
return cloneEnabled;
}
/**
* No flip event is ignored.
*/
protected boolean isIgnoredEvent(MouseEvent e) {
return !isFlipEvent(e) && super.isIgnoredEvent(e);
}
/**
*
*/
protected boolean isFlipEvent(MouseEvent e) {
return false;
}
/**
* 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) {
return graphComponent.getGraph().getEdgeValidationError(state.getCell(), source, target);
}
/**
* Returns true if the current index is 0.
*/
public boolean isSource(int index) {
return index == 0;
}
/**
* Returns true if the current index is the last index.
*/
public boolean isTarget(int index) {
return index == getHandleCount() - 2;
}
/**
* Hides the middle handle if the edge is not bendable.
*/
protected boolean isHandleVisible(int index) {
return super.isHandleVisible(index) && (isSource(index) || isTarget(index) || isCellBendable());
}
/**
*
*/
protected boolean isCellBendable() {
return graphComponent.getGraph().isCellBendable(state.getCell());
}
/**
*
*/
protected Rectangle[] createHandles() {
p = createPoints(state);
Rectangle[] h = new Rectangle[p.length + 1];
for (int i = 0; i < h.length - 1; i++) {
h[i] = createHandle(p[i]);
}
h[p.length] = createHandle(state.getAbsoluteOffset().getPoint(), mxConstants.LABEL_HANDLE_SIZE);
return h;
}
/**
*
*/
protected Color getHandleFillColor(int index) {
boolean source = isSource(index);
if (source || isTarget(index)) {
mxGraph graph = graphComponent.getGraph();
Object terminal = graph.getModel().getTerminal(state.getCell(), source);
if (terminal == null && !graphComponent.getGraph().isTerminalPointMovable(state.getCell(), source)) {
return mxSwingConstants.LOCKED_HANDLE_FILLCOLOR;
}
else if (terminal != null) {
return (graphComponent.getGraph().isCellDisconnectable(state.getCell(), terminal, source))
? mxSwingConstants.CONNECT_HANDLE_FILLCOLOR
: mxSwingConstants.LOCKED_HANDLE_FILLCOLOR;
}
}
return super.getHandleFillColor(index);
}
/**
* @param x
* @param y
* @return Returns the inde of the handle at the given location.
*/
public int getIndexAt(int x, int y) {
int index = super.getIndexAt(x, y);
// Makes the complete label a trigger for the label handle
if (index < 0 && handles != null && handlesVisible && isLabelMovable() && state.getLabelBounds().getRectangle().contains(x, y)) {
index = handles.length - 1;
}
return index;
}
/**
*
*/
protected Rectangle createHandle(Point center) {
return createHandle(center, mxConstants.HANDLE_SIZE);
}
/**
*
*/
protected Rectangle createHandle(Point center, int size) {
return new Rectangle(center.x - size / 2, center.y - size / 2, size, size);
}
/**
*
*/
protected Point[] createPoints(mxCellState s) {
Point[] pts = new Point[s.getAbsolutePointCount()];
for (int i = 0; i < pts.length; i++) {
pts[i] = s.getAbsolutePoint(i).getPoint();
}
return pts;
}
/**
*
*/
protected JComponent createPreview() {
JPanel preview = new JPanel() {
/**
*
*/
private static final long serialVersionUID = -894546588972313020L;
public void paint(Graphics g) {
super.paint(g);
if (!isLabel(index) && p != null) {
((Graphics2D)g).setStroke(mxSwingConstants.PREVIEW_STROKE);
if (isSource(index) || isTarget(index)) {
if (marker.hasValidState() || graphComponent.getGraph().isAllowDanglingEdges()) {
g.setColor(mxSwingConstants.DEFAULT_VALID_COLOR);
}
else {
g.setColor(mxSwingConstants.DEFAULT_INVALID_COLOR);
}
}
else {
g.setColor(Color.BLACK);
}
Point origin = getLocation();
Point last = p[0];
for (int i = 1; i < p.length; i++) {
g.drawLine(last.x - origin.x, last.y - origin.y, p[i].x - origin.x, p[i].y - origin.y);
last = p[i];
}
}
}
};
if (isLabel(index)) {
preview.setBorder(mxSwingConstants.PREVIEW_BORDER);
}
preview.setOpaque(false);
preview.setVisible(false);
return preview;
}
/**
* @param point
* @param gridEnabled
* @return Returns the scaled, translated and grid-aligned point.
*/
protected mxPoint convertPoint(mxPoint point, boolean gridEnabled) {
mxGraph graph = graphComponent.getGraph();
double scale = graph.getView().getScale();
mxPoint trans = graph.getView().getTranslate();
double x = point.getX() / scale - trans.getX();
double y = point.getY() / scale - trans.getY();
if (gridEnabled) {
x = graph.snap(x);
y = graph.snap(y);
}
point.setX(x - state.getOrigin().getX());
point.setY(y - state.getOrigin().getY());
return point;
}
/**
* @return Returns the bounds of the preview.
*/
protected Rectangle getPreviewBounds() {
Rectangle bounds = null;
if (isLabel(index)) {
bounds = state.getLabelBounds().getRectangle();
}
else {
bounds = new Rectangle(p[0]);
for (int i = 0; i < p.length; i++) {
bounds.add(p[i]);
}
bounds.height += 1;
bounds.width += 1;
}
return bounds;
}
/**
*
*/
public void mousePressed(MouseEvent e) {
super.mousePressed(e);
boolean source = isSource(index);
if (source || isTarget(index)) {
mxGraph graph = graphComponent.getGraph();
mxIGraphModel model = graph.getModel();
Object terminal = model.getTerminal(state.getCell(), source);
if ((terminal == null && !graph.isTerminalPointMovable(state.getCell(), source)) ||
(terminal != null && !graph.isCellDisconnectable(state.getCell(), terminal, source))) {
first = null;
}
}
}
/**
*
*/
public void mouseDragged(MouseEvent e) {
if (!e.isConsumed() && first != null) {
gridEnabledEvent = graphComponent.isGridEnabledEvent(e);
constrainedEvent = graphComponent.isConstrainedEvent(e);
boolean isSource = isSource(index);
boolean isTarget = isTarget(index);
Object source = null;
Object target = null;
if (isLabel(index)) {
mxPoint abs = state.getAbsoluteOffset();
double dx = abs.getX() - first.x;
double dy = abs.getY() - first.y;
mxPoint pt = new mxPoint(e.getPoint());
if (gridEnabledEvent) {
pt = graphComponent.snapScaledPoint(pt, dx, dy);
}
if (constrainedEvent) {
if (Math.abs(e.getX() - first.x) > Math.abs(e.getY() - first.y)) {
pt.setY(abs.getY());
}
else {
pt.setX(abs.getX());
}
}
Rectangle rect = getPreviewBounds();
rect.translate((int)Math.round(pt.getX() - first.x), (int)Math.round(pt.getY() - first.y));
preview.setBounds(rect);
}
else {
// Clones the cell state and updates the absolute points using
// the current state of this handle. This is required for
// computing the correct perimeter points and edge style.
mxGeometry geometry = graphComponent.getGraph().getCellGeometry(state.getCell());
mxCellState clone = (mxCellState)state.clone();
List<mxPoint> points = geometry.getPoints();
mxGraphView view = clone.getView();
if (isSource || isTarget) {
marker.process(e);
mxCellState currentState = marker.getValidState();
target = state.getVisibleTerminal(!isSource);
if (currentState != null) {
source = currentState.getCell();
}
else {
mxPoint pt = new mxPoint(e.getPoint());
if (gridEnabledEvent) {
pt = graphComponent.snapScaledPoint(pt);
}
clone.setAbsoluteTerminalPoint(pt, isSource);
}
if (!isSource) {
Object tmp = source;
source = target;
target = tmp;
}
}
else {
mxPoint point = convertPoint(new mxPoint(e.getPoint()), gridEnabledEvent);
if (points == null) {
points = Arrays.asList(new mxPoint[]{point});
}
else if (index - 1 < points.size()) {
points = new ArrayList<mxPoint>(points);
points.set(index - 1, point);
}
source = view.getVisibleTerminal(state.getCell(), true);
target = view.getVisibleTerminal(state.getCell(), false);
}
// Computes the points for the edge style and terminals
mxCellState sourceState = view.getState(source);
mxCellState targetState = view.getState(target);
mxConnectionConstraint sourceConstraint = graphComponent.getGraph().getConnectionConstraint(clone, sourceState, true);
mxConnectionConstraint targetConstraint = graphComponent.getGraph().getConnectionConstraint(clone, targetState, false);
/* TODO: Implement mxConstraintHandler
mxConnectionConstraint constraint = constraintHandler.currentConstraint;
if (constraint == null)
{
constraint = new mxConnectionConstraint();
}
if (isSource)
{
sourceConstraint = constraint;
}
else if (isTarget)
{
targetConstraint = constraint;
}
*/
if (!isSource || sourceState != null) {
view.updateFixedTerminalPoint(clone, sourceState, true, sourceConstraint);
}
if (!isTarget || targetState != null) {
view.updateFixedTerminalPoint(clone, targetState, false, targetConstraint);
}
view.updatePoints(clone, points, sourceState, targetState);
view.updateFloatingTerminalPoints(clone, sourceState, targetState);
// Uses the updated points from the cloned state to draw the preview
p = createPoints(clone);
preview.setBounds(getPreviewBounds());
}
if (!preview.isVisible() && graphComponent.isSignificant(e.getX() - first.x, e.getY() - first.y)) {
preview.setVisible(true);
}
else if (preview.isVisible()) {
preview.repaint();
}
e.consume();
}
}
/**
*
*/
public void mouseReleased(MouseEvent e) {
mxGraph graph = graphComponent.getGraph();
if (!e.isConsumed() && first != null) {
double dx = e.getX() - first.x;
double dy = e.getY() - first.y;
if (graphComponent.isSignificant(dx, dy)) {
if (error != null) {
if (error.length() > 0) {
JOptionPane.showMessageDialog(graphComponent, error);
}
}
else if (isLabel(index)) {
mxPoint abs = state.getAbsoluteOffset();
dx = abs.getX() - first.x;
dy = abs.getY() - first.y;
mxPoint pt = new mxPoint(e.getPoint());
if (gridEnabledEvent) {
pt = graphComponent.snapScaledPoint(pt, dx, dy);
}
if (constrainedEvent) {
if (Math.abs(e.getX() - first.x) > Math.abs(e.getY() - first.y)) {
pt.setY(abs.getY());
}
else {
pt.setX(abs.getX());
}
}
moveLabelTo(state, pt.getX() + dx, pt.getY() + dy);
}
else if (marker.hasValidState() && (isSource(index) || isTarget(index))) {
connect(state.getCell(), marker.getValidState().getCell(), isSource(index), graphComponent.isCloneEvent(e) && isCloneEnabled());
}
else if ((!isSource(index) && !isTarget(index)) || graphComponent.getGraph().isAllowDanglingEdges()) {
movePoint(state.getCell(), index, convertPoint(new mxPoint(e.getPoint()), gridEnabledEvent));
}
e.consume();
}
}
if (!e.isConsumed() && isFlipEvent(e)) {
graph.flipEdge(state.getCell());
e.consume();
}
super.mouseReleased(e);
}
/**
* Extends the implementation to reset the current error and marker.
*/
public void reset() {
super.reset();
marker.reset();
error = null;
}
/**
* Moves the edges control point with the given index to the given point.
*/
protected void movePoint(Object edge, int pointIndex, mxPoint point) {
mxIGraphModel model = graphComponent.getGraph().getModel();
mxGeometry geometry = model.getGeometry(edge);
if (geometry != null) {
model.beginUpdate();
try {
geometry = (mxGeometry)geometry.clone();
if (isSource(index) || isTarget(index)) {
connect(edge, null, isSource(index), false);
geometry.setTerminalPoint(point, isSource(index));
}
else {
List<mxPoint> pts = geometry.getPoints();
if (pts == null) {
pts = new ArrayList<mxPoint>();
geometry.setPoints(pts);
}
if (pts != null) {
if (pointIndex <= pts.size()) {
pts.set(pointIndex - 1, point);
}
else if (pointIndex - 1 <= pts.size()) {
pts.add(pointIndex - 1, point);
}
}
}
model.setGeometry(edge, geometry);
}
finally {
model.endUpdate();
}
}
}
/**
* Connects the given edge to the given source or target terminal.
*
* @param edge
* @param terminal
* @param isSource
*/
protected void connect(Object edge, Object terminal, boolean isSource, boolean isClone) {
mxGraph graph = graphComponent.getGraph();
mxIGraphModel model = graph.getModel();
model.beginUpdate();
try {
if (isClone) {
Object clone = graph.cloneCells(new Object[]{edge})[0];
Object parent = model.getParent(edge);
graph.addCells(new Object[]{clone}, parent);
Object other = model.getTerminal(edge, !isSource);
graph.connectCell(clone, other, !isSource);
graph.setSelectionCell(clone);
edge = clone;
}
// Passes an empty constraint to reset constraint information
graph.connectCell(edge, terminal, isSource, new mxConnectionConstraint());
}
finally {
model.endUpdate();
}
}
/**
* Moves the label to the given position.
*/
protected void moveLabelTo(mxCellState edgeState, double x, double y) {
mxGraph graph = graphComponent.getGraph();
mxIGraphModel model = graph.getModel();
mxGeometry geometry = model.getGeometry(state.getCell());
if (geometry != null) {
geometry = (mxGeometry)geometry.clone();
// Resets the relative location stored inside the geometry
mxPoint pt = graph.getView().getRelativePoint(edgeState, x, y);
geometry.setX(pt.getX());
geometry.setY(pt.getY());
// Resets the offset inside the geometry to find the offset
// from the resulting point
double scale = graph.getView().getScale();
geometry.setOffset(new mxPoint(0, 0));
pt = graph.getView().getPoint(edgeState, geometry);
geometry.setOffset(new mxPoint(Math.round((x - pt.getX()) / scale), Math.round((y - pt.getY()) / scale)));
model.setGeometry(edgeState.getCell(), geometry);
}
}
/**
*
*/
protected Cursor getCursor(MouseEvent e, int index) {
Cursor cursor = null;
if (isLabel(index)) {
cursor = new Cursor(Cursor.MOVE_CURSOR);
}
else {
cursor = new Cursor(Cursor.HAND_CURSOR);
}
return cursor;
}
/**
*
*/
public Color getSelectionColor() {
return mxSwingConstants.EDGE_SELECTION_COLOR;
}
/**
*
*/
public Stroke getSelectionStroke() {
return mxSwingConstants.EDGE_SELECTION_STROKE;
}
/**
*
*/
public void paint(Graphics g) {
Graphics2D g2 = (Graphics2D)g;
Stroke stroke = g2.getStroke();
g2.setStroke(getSelectionStroke());
g.setColor(getSelectionColor());
Point last = state.getAbsolutePoint(0).getPoint();
for (int i = 1; i < state.getAbsolutePointCount(); i++) {
Point current = state.getAbsolutePoint(i).getPoint();
Line2D line = new Line2D.Float(last.x, last.y, current.x, current.y);
Rectangle bounds = g2.getStroke().createStrokedShape(line).getBounds();
if (g.hitClip(bounds.x, bounds.y, bounds.width, bounds.height)) {
g2.draw(line);
}
last = current;
}
g2.setStroke(stroke);
super.paint(g);
}
}