/******************************************************************************* * Copyright (c) 2000, 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.tools; import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; import java.util.List; import org.eclipse.swt.SWT; import org.eclipse.swt.events.KeyEvent; import org.eclipse.swt.graphics.Cursor; import org.eclipse.draw2d.IFigure; import org.eclipse.draw2d.PositionConstants; import org.eclipse.draw2d.geometry.Dimension; import org.eclipse.draw2d.geometry.Point; import org.eclipse.draw2d.geometry.PrecisionPoint; import org.eclipse.draw2d.geometry.PrecisionRectangle; import org.eclipse.gef.AutoexposeHelper; import org.eclipse.gef.EditPart; import org.eclipse.gef.EditPartViewer; import org.eclipse.gef.GraphicalEditPart; import org.eclipse.gef.LayerConstants; import org.eclipse.gef.Request; import org.eclipse.gef.RequestConstants; import org.eclipse.gef.SharedCursors; import org.eclipse.gef.SnapToHelper; import org.eclipse.gef.commands.Command; import org.eclipse.gef.commands.CompoundCommand; import org.eclipse.gef.commands.UnexecutableCommand; import org.eclipse.gef.editparts.LayerManager; import org.eclipse.gef.handles.HandleBounds; import org.eclipse.gef.requests.ChangeBoundsRequest; /** * A DragTracker that moves {@link org.eclipse.gef.EditPart EditParts}. */ public class DragEditPartsTracker extends SelectEditPartTracker { /** * Key modifier for cloning. It's ALT on Mac, and CTRL on all other platforms. */ static final int MODIFIER_CLONE; static { if (SWT.getPlatform().equals("carbon")) //$NON-NLS-1$ MODIFIER_CLONE = SWT.ALT; else MODIFIER_CLONE = SWT.CTRL; } private static final int FLAG_SOURCE_FEEDBACK = SelectEditPartTracker.MAX_FLAG << 1; /** Max flag */ protected static final int MAX_FLAG = FLAG_SOURCE_FEEDBACK; private List exclusionSet; private PrecisionPoint sourceRelativeStartPoint; private SnapToHelper snapToHelper; private PrecisionRectangle sourceRectangle, compoundSrcRect; private boolean cloneActive; /** * Constructs a new DragEditPartsTracker with the given source edit part. * @param sourceEditPart the source edit part */ public DragEditPartsTracker(EditPart sourceEditPart) { super(sourceEditPart); cloneActive = false; setDisabledCursor(SharedCursors.NO); } /** * Returns true if the control key was the key in the key event and the * tool is in an acceptable state for this event. * * @param e the key event * @return true if the key was control and can be accepted. */ private boolean acceptClone(KeyEvent e) { int key = e.keyCode; if (!(isInState(STATE_DRAG_IN_PROGRESS | STATE_ACCESSIBLE_DRAG | STATE_ACCESSIBLE_DRAG_IN_PROGRESS))) return false; return (key == MODIFIER_CLONE); } private boolean acceptSHIFT(KeyEvent e) { return isInState(STATE_DRAG_IN_PROGRESS | STATE_ACCESSIBLE_DRAG | STATE_ACCESSIBLE_DRAG_IN_PROGRESS) && e.keyCode == SWT.SHIFT; } /** * Returns the cursor used under normal conditions. * @see #setDefaultCursor(Cursor) * @return the default cursor */ protected Cursor getDefaultCursor() { if (isCloneActive()) return SharedCursors.CURSOR_TREE_ADD; return super.getDefaultCursor(); } /** * Erases feedback and calls {@link #performDrag()}. Sets the state to terminal. * @see org.eclipse.gef.tools.AbstractTool#commitDrag() */ public void commitDrag() { eraseSourceFeedback(); eraseTargetFeedback(); performDrag(); setState(STATE_TERMINAL); } /** * Captures the bounds of the source being dragged, and the unioned bounds of all figures * being dragged. These bounds are used for snapping by the snap strategies in * <code>updateTargetRequest()</code>. */ private void captureSourceDimensions() { List editparts = getOperationSet(); for (int i = 0; i < editparts.size(); i++) { GraphicalEditPart child = (GraphicalEditPart)editparts.get(i); IFigure figure = child.getFigure(); PrecisionRectangle bounds = null; if (figure instanceof HandleBounds) bounds = new PrecisionRectangle(((HandleBounds)figure).getHandleBounds()); else bounds = new PrecisionRectangle(figure.getBounds()); figure.translateToAbsolute(bounds); if (compoundSrcRect == null) compoundSrcRect = new PrecisionRectangle(bounds); else compoundSrcRect = compoundSrcRect.union(bounds); if (child == getSourceEditPart()) sourceRectangle = bounds; } if (sourceRectangle == null) { IFigure figure = ((GraphicalEditPart)getSourceEditPart()).getFigure(); if (figure instanceof HandleBounds) sourceRectangle = new PrecisionRectangle( ((HandleBounds)figure).getHandleBounds()); else sourceRectangle = new PrecisionRectangle(figure.getBounds()); figure.translateToAbsolute(sourceRectangle); } } /** * Returns a List of top-level edit parts excluding dependants (by calling * {@link ToolUtilities#getSelectionWithoutDependants(EditPartViewer)} that understand the * current target request (by calling * {@link ToolUtilities#filterEditPartsUnderstanding(List, Request)}. * * @see org.eclipse.gef.tools.AbstractTool#createOperationSet() */ protected List createOperationSet() { if (getCurrentViewer() != null) { List list = ToolUtilities.getSelectionWithoutDependants( getCurrentViewer()); ToolUtilities.filterEditPartsUnderstanding(list, getTargetRequest()); return list; } return new ArrayList(); } /** * Creates a {@link ChangeBoundsRequest}. By default, the type is * {@link RequestConstants#REQ_MOVE}. Later on when the edit parts are asked to * contribute to the overall command, the request type will be either * {@link RequestConstants#REQ_MOVE} or {@link RequestConstants#REQ_ORPHAN}, depending on * the result of {@link #isMove()}. * * @see org.eclipse.gef.tools.TargetingTool#createTargetRequest() */ protected Request createTargetRequest() { if (isCloneActive()) return new ChangeBoundsRequest(REQ_CLONE); else return new ChangeBoundsRequest(REQ_MOVE); } /** * Erases source feedback and sets the autoexpose helper to <code>null</code>. * @see org.eclipse.gef.Tool#deactivate() */ public void deactivate() { eraseSourceFeedback(); super.deactivate(); exclusionSet = null; sourceRelativeStartPoint = null; sourceRectangle = null; compoundSrcRect = null; snapToHelper = null; } /** * Asks the edit parts in the {@link AbstractTool#getOperationSet() operation set} to * erase their source feedback. */ protected void eraseSourceFeedback() { if (!getFlag(FLAG_SOURCE_FEEDBACK)) return; setFlag(FLAG_SOURCE_FEEDBACK, false); List editParts = getOperationSet(); for (int i = 0; i < editParts.size(); i++) { EditPart editPart = (EditPart) editParts.get(i); editPart.eraseSourceFeedback(getTargetRequest()); } } /** * Asks each edit part in the {@link AbstractTool#getOperationSet() operation set} to * contribute to a {@link CompoundCommand} after first setting the request type to either * {@link RequestConstants#REQ_MOVE} or {@link RequestConstants#REQ_ORPHAN}, depending on * the result of {@link #isMove()}. * * @see org.eclipse.gef.tools.AbstractTool#getCommand() */ protected Command getCommand() { CompoundCommand command = new CompoundCommand(); command.setDebugLabel("Drag Object Tracker");//$NON-NLS-1$ Iterator iter = getOperationSet().iterator(); Request request = getTargetRequest(); if (isCloneActive()) request.setType(REQ_CLONE); else if (isMove()) request.setType(REQ_MOVE); else request.setType(REQ_ORPHAN); if (!isCloneActive()) { while (iter.hasNext()) { EditPart editPart = (EditPart)iter.next(); command.add(editPart.getCommand(request)); } } if (!isMove() || isCloneActive()) { if (!isCloneActive()) request.setType(REQ_ADD); if (getTargetEditPart() == null) command.add(UnexecutableCommand.INSTANCE); else command.add(getTargetEditPart().getCommand(getTargetRequest())); } return command; } /** * @see org.eclipse.gef.tools.AbstractTool#getCommandName() */ protected String getCommandName() { if (isCloneActive()) return REQ_CLONE; else if (isMove()) return REQ_MOVE; else return REQ_ADD; } /** * @see org.eclipse.gef.tools.AbstractTool#getDebugName() */ protected String getDebugName() { return "DragEditPartsTracker:" + getCommandName();//$NON-NLS-1$ } /** * Returns a list of all the edit parts in the {@link AbstractTool#getOperationSet() * operation set}, plus the {@link org.eclipse.draw2d.ConnectionLayer}. * @see org.eclipse.gef.tools.TargetingTool#getExclusionSet() */ protected Collection getExclusionSet() { if (exclusionSet == null) { List set = getOperationSet(); exclusionSet = new ArrayList(set.size() + 1); for (int i = 0; i < set.size(); i++) { GraphicalEditPart editpart = (GraphicalEditPart)set.get(i); exclusionSet.add(editpart.getFigure()); } LayerManager layerManager = (LayerManager)getCurrentViewer(). getEditPartRegistry().get(LayerManager.ID); if (layerManager != null) { exclusionSet.add(layerManager.getLayer(LayerConstants.CONNECTION_LAYER)); } } return exclusionSet; } /** * @see org.eclipse.gef.tools.TargetingTool#handleAutoexpose() */ protected void handleAutoexpose() { updateTargetRequest(); updateTargetUnderMouse(); showTargetFeedback(); showSourceFeedback(); setCurrentCommand(getCommand()); } /** * Erases feedback and calls {@link #performDrag()}. * @see org.eclipse.gef.tools.AbstractTool#handleButtonUp(int) */ protected boolean handleButtonUp(int button) { if (stateTransition(STATE_DRAG_IN_PROGRESS, STATE_TERMINAL)) { eraseSourceFeedback(); eraseTargetFeedback(); performDrag(); return true; } return super.handleButtonUp(button); } /** * Updates the target request and mouse target, asks to show feedback, and sets the * current command. * @see org.eclipse.gef.tools.AbstractTool#handleDragInProgress() */ protected boolean handleDragInProgress() { if (isInDragInProgress()) { updateTargetRequest(); if (updateTargetUnderMouse()) updateTargetRequest(); showTargetFeedback(); showSourceFeedback(); setCurrentCommand(getCommand()); } return true; } /** * Calls {@link TargetingTool#updateAutoexposeHelper()} if a drag is in progress. * @see org.eclipse.gef.tools.TargetingTool#handleHover() */ protected boolean handleHover() { if (isInDragInProgress()) updateAutoexposeHelper(); return true; } /** * Erases source feedback. * @see org.eclipse.gef.tools.TargetingTool#handleInvalidInput() */ protected boolean handleInvalidInput() { super.handleInvalidInput(); eraseSourceFeedback(); return true; } /** * Processes arrow keys used to move edit parts. * @see org.eclipse.gef.tools.AbstractTool#handleKeyDown(org.eclipse.swt.events.KeyEvent) */ protected boolean handleKeyDown(KeyEvent e) { setAutoexposeHelper(null); if (acceptArrowKey(e)) { accStepIncrement(); if (stateTransition(STATE_INITIAL, STATE_ACCESSIBLE_DRAG_IN_PROGRESS)) setStartLocation(getLocation()); switch (e.keyCode) { case SWT.ARROW_DOWN: placeMouseInViewer(getLocation().getTranslated(0, accGetStep())); break; case SWT.ARROW_UP: placeMouseInViewer(getLocation().getTranslated(0, -accGetStep())); break; case SWT.ARROW_RIGHT: int stepping = accGetStep(); if (isCurrentViewerMirrored()) stepping = -stepping; placeMouseInViewer(getLocation().getTranslated(stepping, 0)); break; case SWT.ARROW_LEFT: int step = -accGetStep(); if (isCurrentViewerMirrored()) step = -step; placeMouseInViewer(getLocation().getTranslated(step, 0)); break; } return true; } else if (acceptClone(e)) { setCloneActive(true); handleDragInProgress(); return true; } else if (acceptSHIFT(e)) { handleDragInProgress(); return true; } return false; } /** * Interprets and processes clone deactivation, constrained move deactivation, and * accessibility navigation reset. * * @see org.eclipse.gef.tools.AbstractTool#handleKeyUp(org.eclipse.swt.events.KeyEvent) */ protected boolean handleKeyUp(KeyEvent e) { if (acceptArrowKey(e)) { accStepReset(); return true; } else if (acceptClone(e)) { setCloneActive(false); handleDragInProgress(); return true; } else if (acceptSHIFT(e)) { handleDragInProgress(); return true; } return false; } /** * Returns true if the current drag is a clone operation. * * @return true if cloning is enabled and is currently active. */ protected boolean isCloneActive() { return cloneActive; } /** * Returns <code>true</code> if the source edit part is being moved within its parent. If * the source edit part is being moved to another parent, this returns <code>false</code>. * @return <code>true</code> if the source edit part is not being reparented */ protected boolean isMove() { EditPart part = getSourceEditPart(); while (part != getTargetEditPart() && part != null) { if (part.getParent() == getTargetEditPart() && part.getSelected() != EditPart.SELECTED_NONE) return true; part = part.getParent(); } return false; } /** * Calls {@link AbstractTool#executeCurrentCommand()}. */ protected void performDrag() { executeCurrentCommand(); } /** * If auto scroll (also called auto expose) is being performed, the start location moves * during the scroll. This method updates that location. */ protected void repairStartLocation() { if (sourceRelativeStartPoint == null) return; IFigure figure = ((GraphicalEditPart)getSourceEditPart()).getFigure(); PrecisionPoint newStart = (PrecisionPoint)sourceRelativeStartPoint.getCopy(); figure.translateToAbsolute(newStart); Point delta = new Point(newStart.x - getStartLocation().x, newStart.y - getStartLocation().y); setStartLocation(newStart); // sourceRectangle and compoundSrcRect need to be updated as well when auto-scrolling if (sourceRectangle != null) sourceRectangle.translate(delta); if (compoundSrcRect != null) compoundSrcRect.translate(delta); } /** * @see org.eclipse.gef.tools.TargetingTool#setAutoexposeHelper(org.eclipse.gef.AutoexposeHelper) */ protected void setAutoexposeHelper(AutoexposeHelper helper) { super.setAutoexposeHelper(helper); if (helper != null && sourceRelativeStartPoint == null && isInDragInProgress()) { IFigure figure = ((GraphicalEditPart)getSourceEditPart()).getFigure(); sourceRelativeStartPoint = new PrecisionPoint(getStartLocation()); figure.translateToRelative(sourceRelativeStartPoint); } } /** * Enables cloning if the value is true. * * @param cloneActive <code>true</code> if cloning should be active */ protected void setCloneActive(boolean cloneActive) { if (this.cloneActive == cloneActive) return; eraseSourceFeedback(); eraseTargetFeedback(); this.cloneActive = cloneActive; } /** * Extended to update the current snap-to strategy. * @see org.eclipse.gef.tools.TargetingTool#setTargetEditPart(org.eclipse.gef.EditPart) */ protected void setTargetEditPart(EditPart editpart) { if (getTargetEditPart() == editpart) return; super.setTargetEditPart(editpart); snapToHelper = null; if (getTargetEditPart() != null && getOperationSet().size() > 0) snapToHelper = (SnapToHelper)getTargetEditPart().getAdapter(SnapToHelper.class); } /** * Asks the edit parts in the {@link AbstractTool#getOperationSet() operation set} to * show source feedback. */ protected void showSourceFeedback() { List editParts = getOperationSet(); for (int i = 0; i < editParts.size(); i++) { EditPart editPart = (EditPart) editParts.get(i); editPart.showSourceFeedback(getTargetRequest()); } setFlag(FLAG_SOURCE_FEEDBACK, true); } /** * Extended to activate cloning and to update the captured source dimensions when * applicable. * @see org.eclipse.gef.tools.AbstractTool#setState(int) */ protected void setState(int state) { boolean check = isInState(STATE_INITIAL); super.setState(state); if (isInState(STATE_ACCESSIBLE_DRAG | STATE_DRAG_IN_PROGRESS | STATE_ACCESSIBLE_DRAG_IN_PROGRESS)) { if (getCurrentInput().isModKeyDown(MODIFIER_CLONE)) { setCloneActive(true); handleDragInProgress(); } } if (check && isInState(STATE_DRAG | STATE_ACCESSIBLE_DRAG | STATE_ACCESSIBLE_DRAG_IN_PROGRESS)) captureSourceDimensions(); } /** * Calls {@link #repairStartLocation()} in case auto scroll is being performed. Updates * the request with the current {@link AbstractTool#getOperationSet() operation set}, * move delta, location and type. * @see org.eclipse.gef.tools.TargetingTool#updateTargetRequest() */ protected void updateTargetRequest() { repairStartLocation(); ChangeBoundsRequest request = (ChangeBoundsRequest)getTargetRequest(); request.setEditParts(getOperationSet()); Dimension delta = getDragMoveDelta(); // constrains the move to dx=0, dy=0, or dx=dy if shift is depressed if (getCurrentInput().isShiftKeyDown()) { request.setConstrainedMove(true); float ratio = 0; if (delta.width != 0) ratio = (float)delta.height / (float)delta.width; ratio = Math.abs(ratio); if (ratio > 0.5 && ratio < 1.5) { if (Math.abs(delta.height) > Math.abs(delta.width)) { if (delta.height > 0) delta.height = Math.abs(delta.width); else delta.height = -Math.abs(delta.width); } else { if (delta.width > 0) delta.width = Math.abs(delta.height); else delta.width = -Math.abs(delta.height); } } else { if (Math.abs(delta.width) > Math.abs(delta.height)) delta.height = 0; else delta.width = 0; } } else request.setConstrainedMove(false); Point moveDelta = new Point(delta.width, delta.height); request.getExtendedData().clear(); request.setMoveDelta(moveDelta); snapPoint(request); request.setLocation(getLocation()); request.setType(getCommandName()); } /** * This method can be overridden by clients to customize the snapping * behavior. * * @param request * the <code>ChangeBoundsRequest</code> from which the move * delta can be extracted and updated * @since 3.4 */ protected void snapPoint(ChangeBoundsRequest request) { Point moveDelta = request.getMoveDelta(); if (snapToHelper != null && !getCurrentInput().isModKeyDown(MODIFIER_NO_SNAPPING)) { PrecisionRectangle baseRect = sourceRectangle.getPreciseCopy(); PrecisionRectangle jointRect = compoundSrcRect.getPreciseCopy(); baseRect.translate(moveDelta); jointRect.translate(moveDelta); PrecisionPoint preciseDelta = new PrecisionPoint(moveDelta); snapToHelper.snapPoint(request, PositionConstants.HORIZONTAL | PositionConstants.VERTICAL, new PrecisionRectangle[] { baseRect, jointRect}, preciseDelta); request.setMoveDelta(preciseDelta); } } }