blob: 2ea07a2645655112469d7c9bcb58b33d2a04e98c [file] [log] [blame]
/**
* Copyright (c) 2006-2015, JGraph Ltd
* Copyright (c) 2006-2015, Gaudenz Alder
*/
/**
* Class: mxConstraintHandler
*
* Handles constraints on connection targets. This class is in charge of
* showing fixed points when the mouse is over a vertex and handles constraints
* to establish new connections.
*
* Constructor: mxConstraintHandler
*
* Constructs an new constraint handler.
*
* Parameters:
*
* graph - Reference to the enclosing <mxGraph>.
* factoryMethod - Optional function to create the edge. The function takes
* the source and target <mxCell> as the first and second argument and
* returns the <mxCell> that represents the new edge.
*/
function mxConstraintHandler(graph)
{
this.graph = graph;
// Adds a graph model listener to update the current focus on changes
this.resetHandler = mxUtils.bind(this, function(sender, evt)
{
if (this.currentFocus != null && this.graph.view.getState(this.currentFocus.cell) == null)
{
this.reset();
}
else
{
this.redraw();
}
});
this.graph.model.addListener(mxEvent.CHANGE, this.resetHandler);
this.graph.view.addListener(mxEvent.SCALE, this.resetHandler);
this.graph.addListener(mxEvent.ROOT, this.resetHandler);
};
/**
* Variable: pointImage
*
* <mxImage> to be used as the image for fixed connection points.
*/
mxConstraintHandler.prototype.pointImage = new mxImage(mxClient.imageBasePath + '/point.gif', 5, 5);
/**
* Variable: graph
*
* Reference to the enclosing <mxGraph>.
*/
mxConstraintHandler.prototype.graph = null;
/**
* Variable: enabled
*
* Specifies if events are handled. Default is true.
*/
mxConstraintHandler.prototype.enabled = true;
/**
* Variable: highlightColor
*
* Specifies the color for the highlight. Default is <mxConstants.DEFAULT_VALID_COLOR>.
*/
mxConstraintHandler.prototype.highlightColor = mxConstants.DEFAULT_VALID_COLOR;
/**
* Function: isEnabled
*
* Returns true if events are handled. This implementation
* returns <enabled>.
*/
mxConstraintHandler.prototype.isEnabled = function()
{
return this.enabled;
};
/**
* Function: setEnabled
*
* Enables or disables event handling. This implementation
* updates <enabled>.
*
* Parameters:
*
* enabled - Boolean that specifies the new enabled state.
*/
mxConstraintHandler.prototype.setEnabled = function(enabled)
{
this.enabled = enabled;
};
/**
* Function: reset
*
* Resets the state of this handler.
*/
mxConstraintHandler.prototype.reset = function()
{
if (this.focusIcons != null)
{
for (var i = 0; i < this.focusIcons.length; i++)
{
this.focusIcons[i].destroy();
}
this.focusIcons = null;
}
if (this.focusHighlight != null)
{
this.focusHighlight.destroy();
this.focusHighlight = null;
}
this.currentConstraint = null;
this.currentFocusArea = null;
this.currentPoint = null;
this.currentFocus = null;
this.focusPoints = null;
};
/**
* Function: getTolerance
*
* Returns the tolerance to be used for intersecting connection points. This
* implementation returns <mxGraph.tolerance>.
*
* Parameters:
*
* me - <mxMouseEvent> whose tolerance should be returned.
*/
mxConstraintHandler.prototype.getTolerance = function(me)
{
return this.graph.getTolerance();
};
/**
* Function: getImageForConstraint
*
* Returns the tolerance to be used for intersecting connection points.
*/
mxConstraintHandler.prototype.getImageForConstraint = function(state, constraint, point)
{
return this.pointImage;
};
/**
* Function: isEventIgnored
*
* Returns true if the given <mxMouseEvent> should be ignored in <update>. This
* implementation always returns false.
*/
mxConstraintHandler.prototype.isEventIgnored = function(me, source)
{
return false;
};
/**
* Function: isStateIgnored
*
* Returns true if the given state should be ignored. This always returns false.
*/
mxConstraintHandler.prototype.isStateIgnored = function(state, source)
{
return false;
};
/**
* Function: destroyIcons
*
* Destroys the <focusIcons> if they exist.
*/
mxConstraintHandler.prototype.destroyIcons = function()
{
if (this.focusIcons != null)
{
for (var i = 0; i < this.focusIcons.length; i++)
{
this.focusIcons[i].destroy();
}
this.focusIcons = null;
this.focusPoints = null;
}
};
/**
* Function: destroyFocusHighlight
*
* Destroys the <focusHighlight> if one exists.
*/
mxConstraintHandler.prototype.destroyFocusHighlight = function()
{
if (this.focusHighlight != null)
{
this.focusHighlight.destroy();
this.focusHighlight = null;
}
};
/**
* Function: isKeepFocusEvent
*
* Returns true if the current focused state should not be changed for the given event.
* This returns true if shift and alt are pressed.
*/
mxConstraintHandler.prototype.isKeepFocusEvent = function(me)
{
return mxEvent.isShiftDown(me.getEvent());
};
/**
* Function: getCellForEvent
*
* Returns the cell for the given event.
*/
mxConstraintHandler.prototype.getCellForEvent = function(me, point)
{
var cell = me.getCell();
// Gets cell under actual point if different from event location
if (cell == null && point != null && (me.getGraphX() != point.x || me.getGraphY() != point.y))
{
cell = this.graph.getCellAt(point.x, point.y);
}
// Uses connectable parent vertex if one exists
if (cell != null && !this.graph.isCellConnectable(cell))
{
var parent = this.graph.getModel().getParent(cell);
if (this.graph.getModel().isVertex(parent) && this.graph.isCellConnectable(parent))
{
cell = parent;
}
}
return cell;
};
/**
* Function: update
*
* Updates the state of this handler based on the given <mxMouseEvent>.
* Source is a boolean indicating if the cell is a source or target.
*/
mxConstraintHandler.prototype.update = function(me, source, existingEdge, point)
{
if (this.isEnabled() && !this.isEventIgnored(me))
{
// Lazy installation of mouseleave handler
if (this.mouseleaveHandler == null && this.graph.container != null)
{
this.mouseleaveHandler = mxUtils.bind(this, function()
{
this.reset();
});
mxEvent.addListener(this.graph.container, 'mouseleave', this.resetHandler);
}
var tol = this.getTolerance(me);
var x = (point != null) ? point.x : me.getGraphX();
var y = (point != null) ? point.y : me.getGraphY();
var grid = new mxRectangle(x - tol, y - tol, 2 * tol, 2 * tol);
var mouse = new mxRectangle(me.getGraphX() - tol, me.getGraphY() - tol, 2 * tol, 2 * tol);
var state = this.graph.view.getState(this.getCellForEvent(me, point));
// Keeps focus icons visible while over vertex bounds and no other cell under mouse or shift is pressed
if (!this.isKeepFocusEvent(me) && (this.currentFocusArea == null || this.currentFocus == null ||
(state != null) || !this.graph.getModel().isVertex(this.currentFocus.cell) ||
!mxUtils.intersects(this.currentFocusArea, mouse)) && (state != this.currentFocus))
{
this.currentFocusArea = null;
this.currentFocus = null;
this.setFocus(me, state, source);
}
this.currentConstraint = null;
this.currentPoint = null;
var minDistSq = null;
if (this.focusIcons != null && this.constraints != null &&
(state == null || this.currentFocus == state))
{
var cx = mouse.getCenterX();
var cy = mouse.getCenterY();
for (var i = 0; i < this.focusIcons.length; i++)
{
var dx = cx - this.focusIcons[i].bounds.getCenterX();
var dy = cy - this.focusIcons[i].bounds.getCenterY();
var tmp = dx * dx + dy * dy;
if ((this.intersects(this.focusIcons[i], mouse, source, existingEdge) || (point != null &&
this.intersects(this.focusIcons[i], grid, source, existingEdge))) &&
(minDistSq == null || tmp < minDistSq))
{
this.currentConstraint = this.constraints[i];
this.currentPoint = this.focusPoints[i];
minDistSq = tmp;
var tmp = this.focusIcons[i].bounds.clone();
tmp.grow(mxConstants.HIGHLIGHT_SIZE);
if (mxClient.IS_IE)
{
tmp.grow(1);
tmp.width -= 1;
tmp.height -= 1;
}
if (this.focusHighlight == null)
{
var hl = this.createHighlightShape();
hl.dialect = (this.graph.dialect == mxConstants.DIALECT_SVG) ?
mxConstants.DIALECT_SVG : mxConstants.DIALECT_VML;
hl.pointerEvents = false;
hl.init(this.graph.getView().getOverlayPane());
this.focusHighlight = hl;
var getState = mxUtils.bind(this, function()
{
return (this.currentFocus != null) ? this.currentFocus : state;
});
mxEvent.redirectMouseEvents(hl.node, this.graph, getState);
}
this.focusHighlight.bounds = tmp;
this.focusHighlight.redraw();
}
}
}
if (this.currentConstraint == null)
{
this.destroyFocusHighlight();
}
}
else
{
this.currentConstraint = null;
this.currentFocus = null;
this.currentPoint = null;
}
};
/**
* Function: redraw
*
* Transfers the focus to the given state as a source or target terminal. If
* the handler is not enabled then the outline is painted, but the constraints
* are ignored.
*/
mxConstraintHandler.prototype.redraw = function()
{
if (this.currentFocus != null && this.constraints != null && this.focusIcons != null)
{
var state = this.graph.view.getState(this.currentFocus.cell);
this.currentFocus = state;
this.currentFocusArea = new mxRectangle(state.x, state.y, state.width, state.height);
for (var i = 0; i < this.constraints.length; i++)
{
var cp = this.graph.getConnectionPoint(state, this.constraints[i]);
var img = this.getImageForConstraint(state, this.constraints[i], cp);
var bounds = new mxRectangle(Math.round(cp.x - img.width / 2),
Math.round(cp.y - img.height / 2), img.width, img.height);
this.focusIcons[i].bounds = bounds;
this.focusIcons[i].redraw();
this.currentFocusArea.add(this.focusIcons[i].bounds);
this.focusPoints[i] = cp;
}
}
};
/**
* Function: setFocus
*
* Transfers the focus to the given state as a source or target terminal. If
* the handler is not enabled then the outline is painted, but the constraints
* are ignored.
*/
mxConstraintHandler.prototype.setFocus = function(me, state, source)
{
this.constraints = (state != null && !this.isStateIgnored(state, source) &&
this.graph.isCellConnectable(state.cell)) ? ((this.isEnabled()) ?
(this.graph.getAllConnectionConstraints(state, source) || []) : []) : null;
// Only uses cells which have constraints
if (this.constraints != null)
{
this.currentFocus = state;
this.currentFocusArea = new mxRectangle(state.x, state.y, state.width, state.height);
if (this.focusIcons != null)
{
for (var i = 0; i < this.focusIcons.length; i++)
{
this.focusIcons[i].destroy();
}
this.focusIcons = null;
this.focusPoints = null;
}
this.focusPoints = [];
this.focusIcons = [];
for (var i = 0; i < this.constraints.length; i++)
{
var cp = this.graph.getConnectionPoint(state, this.constraints[i]);
var img = this.getImageForConstraint(state, this.constraints[i], cp);
var src = img.src;
var bounds = new mxRectangle(Math.round(cp.x - img.width / 2),
Math.round(cp.y - img.height / 2), img.width, img.height);
var icon = new mxImageShape(bounds, src);
icon.dialect = (this.graph.dialect != mxConstants.DIALECT_SVG) ?
mxConstants.DIALECT_MIXEDHTML : mxConstants.DIALECT_SVG;
icon.preserveImageAspect = false;
icon.init(this.graph.getView().getDecoratorPane());
// Fixes lost event tracking for images in quirks / IE8 standards
if (mxClient.IS_QUIRKS || document.documentMode == 8)
{
mxEvent.addListener(icon.node, 'dragstart', function(evt)
{
mxEvent.consume(evt);
return false;
});
}
// Move the icon behind all other overlays
if (icon.node.previousSibling != null)
{
icon.node.parentNode.insertBefore(icon.node, icon.node.parentNode.firstChild);
}
var getState = mxUtils.bind(this, function()
{
return (this.currentFocus != null) ? this.currentFocus : state;
});
icon.redraw();
mxEvent.redirectMouseEvents(icon.node, this.graph, getState);
this.currentFocusArea.add(icon.bounds);
this.focusIcons.push(icon);
this.focusPoints.push(cp);
}
this.currentFocusArea.grow(this.getTolerance(me));
}
else
{
this.destroyIcons();
this.destroyFocusHighlight();
}
};
/**
* Function: createHighlightShape
*
* Create the shape used to paint the highlight.
*
* Returns true if the given icon intersects the given point.
*/
mxConstraintHandler.prototype.createHighlightShape = function()
{
var hl = new mxRectangleShape(null, this.highlightColor, this.highlightColor, mxConstants.HIGHLIGHT_STROKEWIDTH);
hl.opacity = mxConstants.HIGHLIGHT_OPACITY;
return hl;
};
/**
* Function: intersects
*
* Returns true if the given icon intersects the given rectangle.
*/
mxConstraintHandler.prototype.intersects = function(icon, mouse, source, existingEdge)
{
return mxUtils.intersects(icon.bounds, mouse);
};
/**
* Function: destroy
*
* Destroy this handler.
*/
mxConstraintHandler.prototype.destroy = function()
{
this.reset();
if (this.resetHandler != null)
{
this.graph.model.removeListener(this.resetHandler);
this.graph.view.removeListener(this.resetHandler);
this.graph.removeListener(this.resetHandler);
this.resetHandler = null;
}
if (this.mouseleaveHandler != null && this.graph.container != null)
{
mxEvent.removeListener(this.graph.container, 'mouseleave', this.mouseleaveHandler);
this.mouseleaveHandler = null;
}
};