blob: 295653d6ca1aafab47ae3e19c01e162a6176a1e6 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2003, 2010 IBM Corporation and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* IBM Corporation - initial API and implementation
*******************************************************************************/
package org.eclipse.gef;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.eclipse.draw2d.IFigure;
import org.eclipse.draw2d.geometry.PrecisionRectangle;
import org.eclipse.draw2d.geometry.Rectangle;
import org.eclipse.gef.handles.HandleBounds;
import org.eclipse.gef.requests.GroupRequest;
/**
* A temporary helper used to perform snapping to existing elements. This helper
* can be used in conjunction with the
* {@link org.eclipse.gef.tools.DragEditPartsTracker DragEditPartsTracker} when
* dragging editparts within a graphical viewer. Snapping is based on the
* existing children of a <I>container</I>. When snapping a rectangle, the edges
* of the rectangle will snap to edges of other rectangles generated from the
* children of the given container. Similarly, the centers and middles of
* rectangles will snap to each other.
* <P>
* If the snap request is being made during a Move, Reparent or Resize, then the
* figures of the participants of that request will not be used for snapping. If
* the request is a Clone, then the figures for the parts being cloned will be
* used as possible snap locations.
* <P>
* This helper does not keep up with changes made to the container editpart.
* Clients should instantiate a new helper each time one is requested and not
* hold on to instances of the helper.
*
* @since 3.0
* @author Randy Hudson
* @author Pratik Shah
*/
public class SnapToGeometry extends SnapToHelper {
/**
* A property indicating whether this helper should be used. The value
* should be an instance of Boolean. Currently, this class does not check to
* see if the viewer property is set to <code>true</code>.
*
* @see EditPartViewer#setProperty(String, Object)
*/
public static final String PROPERTY_SNAP_ENABLED = "SnapToGeometry.isEnabled"; //$NON-NLS-1$
/**
* The key used to identify the North anchor point in the extended data of a
* request. The north anchor may be set to an {@link Integer} value
* indicating where the snapping is occurring. This is used for feedback
* purposes.
*/
public static final String KEY_NORTH_ANCHOR = "SnapToGeometry.NorthAnchor"; //$NON-NLS-1$
/**
* The key used to identify the South anchor point in the extended data of a
* request. The south anchor may be set to an {@link Integer} value
* indicating where the snapping is occurring. This is used for feedback
* purposes.
*/
public static final String KEY_SOUTH_ANCHOR = "SnapToGeometry.SouthAnchor"; //$NON-NLS-1$
/**
* The key used to identify the West anchor point in the extended data of a
* request. The west anchor may be set to an {@link Integer} value
* indicating where the snapping is occurring. This is used for feedback
* purposes.
*/
public static final String KEY_WEST_ANCHOR = "SnapToGeometry.WestAnchor"; //$NON-NLS-1$
/**
* The key used to identify the East anchor point in the extended data of a
* request. The east anchor may be set to an {@link Integer} value
* indicating where the snapping is occurring. This is used for feedback
* purposes.
*/
public static final String KEY_EAST_ANCHOR = "SnapToGeometry.EastAnchor"; //$NON-NLS-1$
/**
* A vertical or horizontal snapping point. since 3.0
*/
protected static class Entry {
final int type;
final int location;
/**
* Constructs a new entry of the given type and location.
*
* @param type
* an integer between -1 and 1 inclusively
* @param location
* the location
*/
protected Entry(int type, int location) {
if (type < -1 || type > 1)
throw new IllegalArgumentException("Unrecognized snap type"); //$NON-NLS-1$
this.type = type;
this.location = location;
}
/**
* Returns the location of the snap entry.
*
* @return the location
* @since 3.2
*/
public int getLocation() {
return location;
}
/**
* Returns the snap type. The following values may be returned.
* <UL>
* <LI>-1 indicates left/top
* <LI>0 indicates middle/center
* <LI>1 indicates right/bottom
* </UL>
*
* @return the snap type
* @since 3.2
*/
public int getType() {
return type;
}
}
/**
* The sensitivity of the snapping. Corrections greater than this value will
* not occur.
*/
protected static final double THRESHOLD = 5.0001;
private double threshold = THRESHOLD;
boolean cachedCloneBool;
/**
* The horizontal rows being snapped to.
*/
protected Entry rows[];
/**
* The vertical columnd being snapped to.
*/
protected Entry cols[];
/**
* The container editpart providing the coordinates and the children to
* which snapping occurs.
*/
protected GraphicalEditPart container;
/**
* Constructs a helper that will use the given part as its basis for
* snapping. The part's contents pane will provide the coordinate system and
* its children determine the existing elements.
*
* @since 3.0
* @param container
* the container editpart
*/
public SnapToGeometry(GraphicalEditPart container) {
this.container = container;
}
/**
* Get the sensitivity of the snapping. Corrections greater than this value
* will not occur.
*
* @return the snapping threshold
* @since 3.4
*/
protected double getThreshold() {
return this.threshold;
}
/**
* Set the sensitivity of the snapping.
*
* @see #getThreshold()
* @param newThreshold
* the new snapping threshold
* @since 3.4
*/
protected void setThreshold(double newThreshold) {
this.threshold = newThreshold;
}
/**
* Generates a list of parts which should be snapped to. The list is the
* original children, minus the given exclusions, minus and children whose
* figures are not visible.
*
* @since 3.0
* @param exclusions
* the children to exclude
* @return a list of parts which should be snapped to
*/
protected List generateSnapPartsList(List exclusions) {
// Don't snap to any figure that is being dragged
List children = new ArrayList(container.getChildren());
children.removeAll(exclusions);
// Don't snap to hidden figures
List hiddenChildren = new ArrayList();
for (Iterator iter = children.iterator(); iter.hasNext();) {
GraphicalEditPart child = (GraphicalEditPart) iter.next();
if (!child.getFigure().isVisible())
hiddenChildren.add(child);
}
children.removeAll(hiddenChildren);
return children;
}
/**
* Returns the correction value for the given entries and sides. During a
* move, the left, right, or center is free to snap to a location.
*
* @param entries
* the entries
* @param extendedData
* the requests extended data
* @param vert
* <code>true</code> if the correction is vertical
* @param near
* the left/top side of the rectangle
* @param far
* the right/bottom side of the rectangle
* @return the correction amount or #getThreshold () if no correction was
* made
*/
protected double getCorrectionFor(Entry entries[], Map extendedData,
boolean vert, double near, double far) {
far -= 1.0;
double total = near + far;
// If the width is even (i.e., odd right now because we have reduced one
// pixel from
// far) there is no middle pixel so favor the left-most/top-most pixel
// (which is what
// populateRowsAndCols() does by using int precision).
if ((int) (near - far) % 2 != 0)
total -= 1.0;
double result = getCorrectionFor(entries, extendedData, vert,
total / 2, 0);
if (result == getThreshold())
result = getCorrectionFor(entries, extendedData, vert, near, -1);
if (result == getThreshold())
result = getCorrectionFor(entries, extendedData, vert, far, 1);
return result;
}
/**
* Returns the correction value between +/- {@link #getThreshold()}, or the
* #getThreshold () if no corrections were found.
*
* @param entries
* the entries
* @param extendedData
* the map for setting values
* @param vert
* <code>true</code> if vertical
* @param value
* the value being corrected
* @param side
* which sides should be considered
* @return the correction or #getThreshold () if no correction was made
*/
protected double getCorrectionFor(Entry entries[], Map extendedData,
boolean vert, double value, int side) {
double resultMag = getThreshold();
double result = getThreshold();
String property;
if (side == -1)
property = vert ? KEY_WEST_ANCHOR : KEY_NORTH_ANCHOR;
else
property = vert ? KEY_EAST_ANCHOR : KEY_SOUTH_ANCHOR;
for (int i = 0; i < entries.length; i++) {
Entry entry = entries[i];
double magnitude;
if (entry.type == -1 && side != 0) {
magnitude = Math.abs(value - entry.location);
if (magnitude < resultMag) {
resultMag = magnitude;
result = entry.location - value;
extendedData.put(property, new Integer(entry.location));
}
} else if (entry.type == 0 && side == 0) {
magnitude = Math.abs(value - entry.location);
if (magnitude < resultMag) {
resultMag = magnitude;
result = entry.location - value;
extendedData.put(property, new Integer(entry.location));
}
} else if (entry.type == 1 && side != 0) {
magnitude = Math.abs(value - entry.location);
if (magnitude < resultMag) {
resultMag = magnitude;
result = entry.location - value;
extendedData.put(property, new Integer(entry.location));
}
}
}
return result;
}
/**
* Returns the rectangular contribution for the given editpart. This is the
* rectangle with which snapping is performed.
*
* @since 3.0
* @param part
* the child
* @return the rectangular guide for that part
*/
protected Rectangle getFigureBounds(GraphicalEditPart part) {
IFigure fig = part.getFigure();
if (fig instanceof HandleBounds)
return ((HandleBounds) fig).getHandleBounds();
return fig.getBounds();
}
/**
* Updates the cached row and column Entries using the provided parts.
*
* @since 3.0
* @param parts
* a List of EditParts
*/
protected void populateRowsAndCols(List parts) {
rows = new Entry[parts.size() * 3];
cols = new Entry[parts.size() * 3];
for (int i = 0; i < parts.size(); i++) {
GraphicalEditPart child = (GraphicalEditPart) parts.get(i);
Rectangle bounds = getFigureBounds(child);
cols[i * 3] = new Entry(-1, bounds.x);
rows[i * 3] = new Entry(-1, bounds.y);
cols[i * 3 + 1] = new Entry(0, bounds.x + (bounds.width - 1) / 2);
rows[i * 3 + 1] = new Entry(0, bounds.y + (bounds.height - 1) / 2);
cols[i * 3 + 2] = new Entry(1, bounds.right() - 1);
rows[i * 3 + 2] = new Entry(1, bounds.bottom() - 1);
}
}
/**
* @see SnapToHelper#snapRectangle(Request, int, PrecisionRectangle,
* PrecisionRectangle)
*/
public int snapRectangle(Request request, int snapOrientation,
PrecisionRectangle baseRect, PrecisionRectangle result) {
baseRect = baseRect.getPreciseCopy();
makeRelative(container.getContentPane(), baseRect);
PrecisionRectangle correction = new PrecisionRectangle();
makeRelative(container.getContentPane(), correction);
// Recalculate snapping locations if needed
boolean isClone = request.getType().equals(RequestConstants.REQ_CLONE);
if (rows == null || cols == null || isClone != cachedCloneBool) {
cachedCloneBool = isClone;
List exclusionSet = Collections.EMPTY_LIST;
if (!isClone && request instanceof GroupRequest)
exclusionSet = ((GroupRequest) request).getEditParts();
populateRowsAndCols(generateSnapPartsList(exclusionSet));
}
if ((snapOrientation & HORIZONTAL) != 0) {
double xcorrect = getThreshold();
xcorrect = getCorrectionFor(cols, request.getExtendedData(), true,
baseRect.preciseX(), baseRect.preciseRight());
if (xcorrect != getThreshold()) {
snapOrientation &= ~HORIZONTAL;
correction.setPreciseX(correction.preciseX() + xcorrect);
}
}
if ((snapOrientation & VERTICAL) != 0) {
double ycorrect = getThreshold();
ycorrect = getCorrectionFor(rows, request.getExtendedData(), false,
baseRect.preciseY(), baseRect.preciseBottom());
if (ycorrect != getThreshold()) {
snapOrientation &= ~VERTICAL;
correction.setPreciseY(correction.preciseY() + ycorrect);
}
}
if ((snapOrientation & EAST) != 0) {
double rightCorrection = getCorrectionFor(cols,
request.getExtendedData(), true,
baseRect.preciseRight() - 1, 1);
if (rightCorrection != getThreshold()) {
snapOrientation &= ~EAST;
correction.setPreciseWidth(correction.preciseWidth()
+ rightCorrection);
}
}
if ((snapOrientation & WEST) != 0) {
double leftCorrection = getCorrectionFor(cols,
request.getExtendedData(), true, baseRect.preciseX(), -1);
if (leftCorrection != getThreshold()) {
snapOrientation &= ~WEST;
correction.setPreciseWidth(correction.preciseWidth()
- leftCorrection);
correction.setPreciseX(correction.preciseX() + leftCorrection);
}
}
if ((snapOrientation & SOUTH) != 0) {
double bottom = getCorrectionFor(rows, request.getExtendedData(),
false, baseRect.preciseBottom() - 1, 1);
if (bottom != getThreshold()) {
snapOrientation &= ~SOUTH;
correction
.setPreciseHeight(correction.preciseHeight() + bottom);
}
}
if ((snapOrientation & NORTH) != 0) {
double topCorrection = getCorrectionFor(rows,
request.getExtendedData(), false, baseRect.preciseY(), -1);
if (topCorrection != getThreshold()) {
snapOrientation &= ~NORTH;
correction.setPreciseHeight(correction.preciseHeight()
- topCorrection);
correction.setPreciseY(correction.preciseY() + topCorrection);
}
}
makeAbsolute(container.getContentPane(), correction);
result.setPreciseX(result.preciseX() + correction.preciseX());
result.setPreciseY(result.preciseY() + correction.preciseY());
result.setPreciseWidth(result.preciseWidth()
+ correction.preciseWidth());
result.setPreciseHeight(result.preciseHeight()
+ correction.preciseHeight());
return snapOrientation;
}
}