/******************************************************************************* * 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.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.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.preciseY += ycorrect; } } if ((snapOrientation & EAST) != 0) { double rightCorrection = getCorrectionFor(cols, request.getExtendedData(), true, baseRect.preciseRight() - 1, 1); if (rightCorrection != getThreshold()) { snapOrientation &= ~EAST; correction.preciseWidth += rightCorrection; } } if ((snapOrientation & WEST) != 0) { double leftCorrection = getCorrectionFor(cols, request.getExtendedData(), true, baseRect.preciseX, -1); if (leftCorrection != getThreshold()) { snapOrientation &= ~WEST; correction.preciseWidth -= leftCorrection; correction.preciseX += leftCorrection; } } if ((snapOrientation & SOUTH) != 0) { double bottom = getCorrectionFor(rows, request.getExtendedData(), false, baseRect.preciseBottom() - 1, 1); if (bottom != getThreshold()) { snapOrientation &= ~SOUTH; correction.preciseHeight += bottom; } } if ((snapOrientation & NORTH) != 0) { double topCorrection = getCorrectionFor(rows, request.getExtendedData(), false, baseRect.preciseY, -1); if (topCorrection != getThreshold()) { snapOrientation &= ~NORTH; correction.preciseHeight -= topCorrection; correction.preciseY += topCorrection; } } 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; } }