/*******************************************************************************
* Copyright (c) 2003, 2007 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.Map;
import org.eclipse.draw2d.geometry.PrecisionRectangle;
import org.eclipse.gef.requests.GroupRequest;
import org.eclipse.gef.rulers.RulerProvider;
/**
* A helper used to perform snapping to guides. The guides are obtained from the viewer's
* horizontal and vertical {@link RulerProvider RulerProviders}. If snapping is
* performed, the request's extended data will contain keyed values indicating which
* guides were snapped to, and which side of the part should be attached. Generally
* snapping to a guide should attach the part to that guide, but application behavior may
* differ.
* <P>
* Snapping (and attaching) to a guide is only possible if a single part is being dragged.
* The current implementation will not snap if a request contains multiple parts. This may
* be relaxed in the future to allow snapping, but without setting the attachment extended
* data.
* <P>
* This helper does not keep up with changes in guides. 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 SnapToGuides
extends SnapToHelper
{
/**
* The key used to identify the Vertical Guide. This key is used with the request's
* extended data map to store an Integer. The integer value is the location of the guide
* that is being snapped to.
*/
public static final String KEY_VERTICAL_GUIDE = "SnapToGuides.VerticalGuide"; //$NON-NLS-1$
/**
* The key used to identify the Horizontal Guide. This key is used with the request's
* extended data map to store an Integer. The integer value is the location of the guide
* that is being snapped to.
*/
public static final String KEY_HORIZONTAL_GUIDE = "SnapToGuides.HorizontalGuide"; //$NON-NLS-1$
/**
* The key used to identify the vertical anchor point. This key is used with the
* request's extended data map to store an Integer. If the VERTICAL_GUIDE has been set,
* then this integer is a number identifying which side of the dragged object is being
* snapped to that guide.
* <UL>
* <LI><code>-1</code> indicates the left side should be attached.
* <LI><code> 0</code> indicates the center should be attached.
* <LI><code> 1</code> indicates the right side should be attached.
* </UL>
*/
public static final String KEY_VERTICAL_ANCHOR = "SnapToGuides.VerticalAttachment"; //$NON-NLS-1$
/**
* The key used to identify the horizontal anchor point. This key is used with the
* request's extended data map to store an Integer. If the HORIZONTAL_GUIDE has been set,
* then this integer is a number identifying which side of the dragged object is being
* snapped to that guide.
* <UL>
* <LI><code>-1</code> indicates the top side should be attached.
* <LI><code> 0</code> indicates the middle should be attached.
* <LI><code> 1</code> indicates the bottom side should be attached.
* </UL>
*/
public static final String KEY_HORIZONTAL_ANCHOR = "SnapToGuides.HorizontalAttachment";//$NON-NLS-1$
/**
* The threshold for snapping to guides. The rectangle being snapped must be within +/-
* the THRESHOLD. The default value is 5.001;
*/
protected static final double THRESHOLD = 5.001;
private double threshold = THRESHOLD;
/**
* The graphical editpart to which guides are relative. This should also the parent of
* the parts being snapped to guides.
*/
protected GraphicalEditPart container;
/**
* The locations of the vertical guides in the container's coordinates. Use {@link
* #getVerticalGuides()}.
*/
protected int[] verticalGuides;
/**
* The locations of the horizontal guides in the container's coordinates. Use {@link
* #getHorizontalGuides()}.
*/
protected int[] horizontalGuides;
/**
* Constructs a new snap-to-guides helper using the given container as the basis.
* @param container the container editpart
*/
public SnapToGuides(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;
}
/**
* Returns the correction for the given near and far sides of a rectangle or {@link
* #getThreshold()} if no correction was found. The near side represents the top or left side
* of a rectangle being snapped. Similar for far. If snapping occurs, the extendedData
* will have the guide and attachment point set.
*
* @param guides the location of the guides
* @param near the top or left location
* @param far the bottom or right location
* @param extendedData the map for storing snap details
* @param isVertical <code>true</code> if for vertical guides, <code>false</code>
* for horizontal.
* @return the correction amount or getThreshold() if no correction was made
*/
protected double getCorrectionFor(int[] guides, double near, double far, Map extendedData,
boolean isVertical) {
far -= 1.0;
double total = near + far;
//If the width is even, there is no middle pixel so favor the left - most pixel.
if ((int)(near - far) % 2 == 0)
total -= 1.0;
double result = getCorrectionFor(guides, total / 2, extendedData, isVertical, 0);
if (result == getThreshold())
result = getCorrectionFor(guides, near, extendedData, isVertical, -1);
if (result == getThreshold())
result = getCorrectionFor(guides, far, extendedData, isVertical, 1);
return result;
}
/**
* Returns the correction for the given location or {@link #getThreshold()} if no correction
* was found. If correction occurs, the extendedData will have the guide and attachment
* point set. The attachment point is identified by the <code>side</code> parameter.
* <P>
* The correction's magnitude will be less than getThreshold().
*
* @param guides the location of the guides
* @param value the location being tested
* @param extendedData the map for storing snap details
* @param vert <code>true</code> if for vertical guides, <code>false</code>
* @param side the integer indicating which side is being snapped
* @return a correction amount or getThreshold() if no correction was made
*/
protected double getCorrectionFor(int[] guides, double value, Map extendedData,
boolean vert, int side) {
double resultMag = getThreshold();
double result = getThreshold();
for (int i = 0; i < guides.length; i++) {
int offset = guides[i];
double magnitude;
magnitude = Math.abs(value - offset);
if (magnitude < resultMag) {
extendedData.put(vert ? KEY_VERTICAL_GUIDE : KEY_HORIZONTAL_GUIDE,
new Integer(guides[i]));
extendedData.put(vert ? KEY_VERTICAL_ANCHOR : KEY_HORIZONTAL_ANCHOR,
new Integer(side));
resultMag = magnitude;
result = offset - value;
}
}
return result;
}
/**
* Returns the horizontal guides in the coordinates of the container's contents pane.
* @return the horizontal guides
*/
protected int[] getHorizontalGuides() {
if (horizontalGuides == null) {
RulerProvider rProvider = ((RulerProvider)container.getViewer()
.getProperty(RulerProvider.PROPERTY_VERTICAL_RULER));
if (rProvider != null)
horizontalGuides = rProvider.getGuidePositions();
else
horizontalGuides = new int[0];
}
return horizontalGuides;
}
/**
* Returns the vertical guides in the coordinates of the container's contents pane.
* @return the vertical guides
*/
protected int[] getVerticalGuides() {
if (verticalGuides == null) {
RulerProvider rProvider = ((RulerProvider)container.getViewer()
.getProperty(RulerProvider.PROPERTY_HORIZONTAL_RULER));
if (rProvider != null)
verticalGuides = rProvider.getGuidePositions();
else
verticalGuides = new int[0];
}
return verticalGuides;
}
/**
* @see SnapToHelper#snapRectangle(Request, int, PrecisionRectangle, PrecisionRectangle)
*/
public int snapRectangle(Request request, int snapOrientation,
PrecisionRectangle baseRect, PrecisionRectangle result) {
if (request instanceof GroupRequest
&& ((GroupRequest)request).getEditParts().size() > 1)
return snapOrientation;
baseRect = baseRect.getPreciseCopy();
makeRelative(container.getContentPane(), baseRect);
PrecisionRectangle correction = new PrecisionRectangle();
makeRelative(container.getContentPane(), correction);
if ((snapOrientation & HORIZONTAL) != 0) {
double xcorrect = getCorrectionFor(getVerticalGuides(), baseRect.preciseX,
baseRect.preciseRight(), request.getExtendedData(), true);
if (xcorrect != getThreshold()) {
snapOrientation &= ~HORIZONTAL;
correction.preciseX += xcorrect;
}
}
if ((snapOrientation & VERTICAL) != 0) {
double ycorrect = getCorrectionFor(getHorizontalGuides(), baseRect.preciseY,
baseRect.preciseBottom(), request.getExtendedData(), false);
if (ycorrect != getThreshold()) {
snapOrientation &= ~VERTICAL;
correction.preciseY += ycorrect;
}
}
boolean snapped = false;
if (!snapped && (snapOrientation & WEST) != 0) {
double leftCorrection = getCorrectionFor(getVerticalGuides(),
baseRect.preciseX, request.getExtendedData(), true, -1);
if (leftCorrection != getThreshold()) {
snapOrientation &= ~WEST;
correction.preciseWidth -= leftCorrection;
correction.preciseX += leftCorrection;
}
}
if (!snapped && (snapOrientation & EAST) != 0) {
double rightCorrection = getCorrectionFor(getVerticalGuides(),
baseRect.preciseRight() - 1, request.getExtendedData(), true, 1);
if (rightCorrection != getThreshold()) {
snapped = true;
snapOrientation &= ~EAST;
correction.preciseWidth += rightCorrection;
}
}
snapped = false;
if (!snapped && (snapOrientation & NORTH) != 0) {
double topCorrection = getCorrectionFor(getHorizontalGuides(),
baseRect.preciseY, request.getExtendedData(), false, -1);
if (topCorrection != getThreshold()) {
snapOrientation &= ~NORTH;
correction.preciseHeight -= topCorrection;
correction.preciseY += topCorrection;
}
}
if (!snapped && (snapOrientation & SOUTH) != 0) {
double bottom = getCorrectionFor(getHorizontalGuides(),
baseRect.preciseBottom() - 1, request.getExtendedData(), false, 1);
if (bottom != getThreshold()) {
snapped = true;
snapOrientation &= ~SOUTH;
correction.preciseHeight += bottom;
}
}
correction.updateInts();
makeAbsolute(container.getContentPane(), correction);
result.preciseX += correction.preciseX;
result.preciseY += correction.preciseY;
result.preciseWidth += correction.preciseWidth;
result.preciseHeight += correction.preciseHeight;
result.updateInts();
return snapOrientation;
}
}