/******************************************************************************* * Copyright (c) 2003, 2010, 2012 IBM Corporation, Gerhardt Informatics Kft. 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 * Gerhardt Informatics Kft. - GEFGWT port *******************************************************************************/ 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; } }