/** * Copyright (c) 2002-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 - Initial API and implementation */ package org.eclipse.emf.edit.command; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.ListIterator; import org.eclipse.emf.common.command.AbstractCommand; import org.eclipse.emf.common.command.Command; import org.eclipse.emf.common.command.CommandWrapper; import org.eclipse.emf.common.command.CompoundCommand; import org.eclipse.emf.common.command.IdentityCommand; import org.eclipse.emf.common.command.UnexecutableCommand; import org.eclipse.emf.ecore.EReference; import org.eclipse.emf.ecore.EStructuralFeature; import org.eclipse.emf.edit.EMFEditPlugin; import org.eclipse.emf.edit.domain.AdapterFactoryEditingDomain; import org.eclipse.emf.edit.domain.EditingDomain; /** * The drag and drop command logically acts upon an owner object onto which a collection of things is being dragged. * The static create method delegates command creation to {@link EditingDomain#createCommand EditingDomain.createCommand}, * which may or may not result in the actual creation of an instance of this class. * <p> * The implementation of this class is high-level and generic; * it ultimately delegates all behaviour to other types of command, * and is typically undoable as a result. */ public class DragAndDropCommand extends AbstractCommand implements DragAndDropFeedback { /** * This class is used to encode the drag and drop arguments * into an object that will be passed as the feature of a {@link CommandParameter}. */ public static class Detail { public float location; public int operations; public int operation; public Detail(float location, int operations, int operation) { this.location = location; this.operations = operations; this.operation = operation; } } /** * This creates a command to perform a drag and drop operation upon the owner. * See {@link DragAndDropCommand DragAndDropCommand} for a description of the arguments. */ public static Command create(EditingDomain domain, Object owner, float location, int operations, int operation, Collection<?> collection) { return domain.createCommand (DragAndDropCommand.class, new CommandParameter(owner, new Detail(location, operations, operation), collection)); } /** * This caches the label. */ protected static final String LABEL = EMFEditPlugin.INSTANCE.getString("_UI_DragAndDropCommand_label"); /** * This caches the description. */ protected static final String DESCRIPTION = EMFEditPlugin.INSTANCE.getString("_UI_DragAndDropCommand_description"); /** * This keeps track of the domain in which this command is created. */ protected EditingDomain domain; /** * This keeps track of the owner that is the target of the drag and drop. */ protected Object owner; /** * This keeps track of the location of the drag and drop. */ protected float location; /** * This keeps track of the lower range of locations in which the effect of this command remains unchanged. */ protected float lowerLocationBound; /** * This keeps track of the upper range of locations in which the effect of this command remains unchanged. */ protected float upperLocationBound; /** * This keeps track of the permitted operations. */ protected int operations; /** * This keeps track of the current operation that will be returned by {@link #getOperation}. */ protected int operation; /** * This keeps track of the feedback that will be returned by {@link #getFeedback}. */ protected int feedback; /** * This keeps track of the collection of dragged sources. */ protected Collection<?> collection; /** * This keeps track of the command that implements the drag side of the operation. */ protected Command dragCommand; /** * This keeps track of whether execute has been called on the {@link #dragCommand}. */ protected boolean isDragCommandExecuted; /** * This keeps track of the command that implements the drop side of the operation. */ protected Command dropCommand; /** * This controls whether or not to optimize the {@link #prepare}. */ protected boolean optimize; /** * This is to remember which owner to use for the drop command in the optimized mode. */ protected Object optimizedDropCommandOwner; /** * This creates and instance in the given domain and for the given information. * The location should be in the range of 0.0 to 1.0, * indicating the relative vertical location of the drag operation, * where 0.0 is at the top and 1.0 is at the bottom. * The operations is a bitwise mask of the DROP_* values. * The operation is the desired operation as specified by a DROP_* value. * And the collection contains the source objects being dragged. */ public DragAndDropCommand(EditingDomain domain, Object owner, float location, int operations, int operation, Collection<?> collection) { this(domain, owner, location, operations, operation, collection, domain == null ? false : domain.getOptimizeCopy()); } public DragAndDropCommand (EditingDomain domain, Object owner, float location, int operations, int operation, Collection<?> collection, boolean optimize) { super(LABEL, DESCRIPTION); this.domain = domain; this.owner = owner; this.location = location; this.operations = operations; this.operation = operation; this.collection = collection; this.optimize = optimize; // EATM Leave this disabled for now. // this.optimize = false; } protected boolean analyzeForNonContainment(Command command) { if (command instanceof AddCommand) { return isNonContainment(((AddCommand)command).getFeature()); } else if (command instanceof SetCommand) { return isNonContainment(((SetCommand)command).getFeature()); } else if (command instanceof CommandWrapper) { return analyzeForNonContainment(((CommandWrapper)command).getCommand()); } else if (command instanceof CompoundCommand) { for (Command childCommand : ((CompoundCommand)command).getCommandList()) { if (analyzeForNonContainment(childCommand)) { return true; } } } return false; } protected boolean isNonContainment(EStructuralFeature feature) { return feature instanceof EReference && !((EReference)feature).isContainment(); } public Object getOwner() { return owner; } public float getLocation() { return location; } public int getOperations() { return operations; } public Collection<?> getCollection() { return collection; } /** * This implementation of prepare is called again to implement {@link #validate validate}. * The method {@link #reset} will have been called before doing so. */ @Override protected boolean prepare() { // We'll default to this. // boolean result = false; // If there isn't something obviously wrong with the arguments... // if (owner != null && collection != null && operations != DROP_NONE && operation != DROP_NONE) { // If the location is near the boundary, we'll start by trying to do a drop insert. // if (location <= 0.20 || location >= 0.80) { // If we could do a drop insert operation... // result = prepareDropInsert(); if (result) { // Set the bounds so that we re-check when we are closer to the middle. // if (location <= 0.20) { lowerLocationBound = 0.0F; upperLocationBound = 0.2F; } else { lowerLocationBound = 0.8F; upperLocationBound = 1.0F; } } else { // We can try to do a drop on instead. // reset(); result = prepareDropOn(); // Set the bounds so that we re-check when we get near the other end. // if (location <= 0.20) { lowerLocationBound = 0.0F; upperLocationBound = 0.8F; } else { lowerLocationBound = 0.2F; upperLocationBound = 1.0F; } } } // We are near the middle, so we'll start by trying to do a drop on. // else { // If we can do a drop on operation. // result = prepareDropOn(); if (result) { // Set the range so that we re-check when we get aren't in the middle. // lowerLocationBound = 0.2F; upperLocationBound = 0.8F; } else { // We can reset and try a drop insert instead. // reset(); result = prepareDropInsert(); // Set the range so that we re-check when we get into the other half. // if (location <= 0.50) { lowerLocationBound = 0.0F; upperLocationBound = 0.5F; } else { lowerLocationBound = 0.5F; upperLocationBound = 1.0F; } } } } else { // We'll always be wrong for these arguments, so don't bother re-checking. // lowerLocationBound = 0.0F; upperLocationBound = 1.0F; } return result; } /** * This can be overridden to determine the parent of an object; this implementation uses {@link EditingDomain#getParent}. */ protected Object getParent(Object object) { return domain.getParent(object); } /** * This can be overridden to determine the children of an object; this implementation uses {@link EditingDomain#getChildren}. */ protected Collection<?> getChildren(Object object) { return domain.getChildren(object); } /** * This attempts to prepare a drop insert operation. */ protected boolean prepareDropInsert() { // This will be the default return value. // boolean result = false; // The feedback is set based on which half we are in. // If the command isn't executable, these values won't be used. // feedback = location < 0.5 ? FEEDBACK_INSERT_BEFORE : FEEDBACK_INSERT_AFTER; // If we can't determine the parent. // Object parent = getParent(owner); if (parent == null) { dragCommand = UnexecutableCommand.INSTANCE; dropCommand = UnexecutableCommand.INSTANCE; } else { // Iterate over the children to find the owner. // Collection<?> children = getChildren(parent); int i = 0; for (Object child : children) { // When we match the owner, we're done. // if (child == owner) { break; } ++i; } // If the location indicates after, add one more. // if (location >= 0.5) { ++i; } // Try to create a specific command based on the current desired operation. // switch (operation) { case DROP_MOVE: { result = prepareDropMoveInsert(parent, children, i); break; } case DROP_COPY: { result = prepareDropCopyInsert(parent, children, i); break; } case DROP_LINK: { result = prepareDropLinkInsert(parent, children, i); break; } } // If there isn't an executable command we should maybe try a copy operation, but only if we're allowed and not doing a link. // if (!result && operation != DROP_COPY && operation != DROP_LINK && (operations & DROP_COPY) != 0) { // Try again. // reset(); result = prepareDropCopyInsert(parent, children, i); if (result) { // We've switch the operation! // operation = DROP_COPY; } } // If there isn't an executable command we should maybe try a link operation, but only if we're allowed and not doing a link. // if (!result && operation != DROP_LINK && (operations & DROP_LINK) != 0) { // Try again. // reset(); result = prepareDropLinkInsert(parent, children, i); if (result) { // We've switch the operation! // operation = DROP_LINK; } } } return result; } /** * This attempts to prepare a drop move insert operation. */ protected boolean prepareDropMoveInsert(Object parent, Collection<?> children, int index) { // We don't want to move insert an object before or after itself... // if (collection.contains(owner)) { dragCommand = IdentityCommand.INSTANCE; dropCommand = UnexecutableCommand.INSTANCE; } // If the dragged objects share a parent... // else if (children.containsAll(collection)) { dragCommand = IdentityCommand.INSTANCE; // Create move commands for all the objects in the collection. // CompoundCommand compoundCommand = new CompoundCommand(); List<Object> before = new ArrayList<Object>(); List<Object> after = new ArrayList<Object>(); int j = 0; for (Object object : children) { if (collection.contains(object)) { if (j < index) { before.add(object); } else if (j > index) { after.add(object); } } ++j; } for (Object object : before) { compoundCommand.append(MoveCommand.create(domain, parent, null, object, index - 1)); } for (ListIterator<Object> objects = after.listIterator(after.size()); objects.hasPrevious(); ) { Object object = objects.previous(); compoundCommand.append(MoveCommand.create(domain, parent, null, object, index)); } dropCommand = compoundCommand.getCommandList().size() == 0 ? (Command)IdentityCommand.INSTANCE : compoundCommand; } else if (isCrossDomain()) { dragCommand = IdentityCommand.INSTANCE; dropCommand = UnexecutableCommand.INSTANCE; } else { // Just remove the objects and add them. // dropCommand = AddCommand.create(domain, parent, null, collection, index); if (analyzeForNonContainment(dropCommand)) { dropCommand.dispose(); dropCommand = UnexecutableCommand.INSTANCE; dragCommand = IdentityCommand.INSTANCE; } else { dragCommand = RemoveCommand.create(domain, collection); } } boolean result = dragCommand.canExecute() && dropCommand.canExecute(); return result; } protected boolean isCrossDomain() { for (Object item : collection) { EditingDomain itemDomain = AdapterFactoryEditingDomain.getEditingDomainFor(item); if (itemDomain != null && itemDomain != domain) { return true; } } return false; } /** * This attempts to prepare a drop copy insert operation. */ protected boolean prepareDropCopyInsert(final Object parent, Collection<?> children, final int index) { boolean result; // We don't want to copy insert an object before or after itself... // if (collection.contains(owner)) { dragCommand = IdentityCommand.INSTANCE; dropCommand = UnexecutableCommand.INSTANCE; result = false; } else { // Copy the collection // dragCommand = CopyCommand.create(domain, collection); if (optimize) { result = optimizedCanExecute(); if (result) { optimizedDropCommandOwner = parent; } } else { if (dragCommand.canExecute() && dragCommand.canUndo()) { dragCommand.execute(); isDragCommandExecuted = true; // And add the copy. // dropCommand = AddCommand.create(domain, parent, null, dragCommand.getResult(), index); if (analyzeForNonContainment(dropCommand)) { dropCommand.dispose(); dropCommand = UnexecutableCommand.INSTANCE; dragCommand.undo(); dragCommand.dispose(); isDragCommandExecuted = false; dragCommand = IdentityCommand.INSTANCE; } result = dropCommand.canExecute(); } else { dropCommand = UnexecutableCommand.INSTANCE; result = false; } } // if optimize } // if collection return result; } /** * This attempts to prepare a drop link insert operation. */ protected boolean prepareDropLinkInsert(Object parent, Collection<?> children, int index) { boolean result; // We don't want to insert an object before or after itself... // if (collection.contains(owner)) { dragCommand = IdentityCommand.INSTANCE; dropCommand = UnexecutableCommand.INSTANCE; result = false; } else { dragCommand = IdentityCommand.INSTANCE; // Add the collection // dropCommand = AddCommand.create(domain, parent, null, collection, index); if (!analyzeForNonContainment(dropCommand)) { dropCommand.dispose(); dropCommand = UnexecutableCommand.INSTANCE; } result = dropCommand.canExecute(); } return result; } /** * This attempts to prepare a drop on operation. */ protected boolean prepareDropOn() { // This is the default return value. // boolean result = false; // This is the feedback we use to indicate drop on; it will only be used if the command is executable. // feedback = FEEDBACK_SELECT; // Prepare the right type of operation. // switch (operation) { case DROP_MOVE: { result = prepareDropMoveOn(); break; } case DROP_COPY: { result = prepareDropCopyOn(); break; } case DROP_LINK: { result = prepareDropLinkOn(); break; } } // If there isn't an executable command we should maybe try a copy operation, but only if we're allowed and not doing a link. // if (!result && operation != DROP_COPY && operation != DROP_LINK && (operations & DROP_COPY) != 0) { reset(); result = prepareDropCopyOn(); if (result) { operation = DROP_COPY; } } // If there isn't an executable command we should maybe try a link operation, but only if we're allowed and not doing a link. // if (!result && operation != DROP_LINK && (operations & DROP_LINK) != 0) { reset(); result = prepareDropLinkOn(); if (result) { operation = DROP_LINK; } } return result; } /** * This attempts to prepare a drop move on operation. */ protected boolean prepareDropMoveOn() { if (isCrossDomain()) { dragCommand = IdentityCommand.INSTANCE; dropCommand = UnexecutableCommand.INSTANCE; } else { dropCommand = AddCommand.create(domain, owner, null, collection); if (analyzeForNonContainment(dropCommand)) { dropCommand.dispose(); dropCommand = UnexecutableCommand.INSTANCE; dragCommand = IdentityCommand.INSTANCE; } else { dragCommand = RemoveCommand.create(domain, collection); } } boolean result = dragCommand.canExecute() && dropCommand.canExecute(); return result; } /** * This attempts to prepare a drop copy on operation. */ protected boolean prepareDropCopyOn() { boolean result; dragCommand = CopyCommand.create(domain, collection); if (optimize) { result = optimizedCanExecute(); if (result) { optimizedDropCommandOwner = owner; } } else { if (dragCommand.canExecute() && dragCommand.canUndo()) { dragCommand.execute(); isDragCommandExecuted = true; dropCommand = AddCommand.create(domain, owner, null, dragCommand.getResult()); if (analyzeForNonContainment(dropCommand)) { dropCommand.dispose(); dropCommand = UnexecutableCommand.INSTANCE; dragCommand.undo(); dragCommand.dispose(); isDragCommandExecuted = false; dragCommand = IdentityCommand.INSTANCE; } } else { dropCommand = UnexecutableCommand.INSTANCE; } result = dragCommand.canExecute() && dropCommand.canExecute(); } return result; } /** * This attempts to prepare a drop link on operation. */ protected boolean prepareDropLinkOn() { dragCommand = IdentityCommand.INSTANCE; dropCommand = SetCommand.create(domain, owner, null, collection); // If we can't set the collection, try setting use the single value of the collection. // if (!dropCommand.canExecute() && collection.size() == 1) { dropCommand.dispose(); dropCommand = SetCommand.create(domain, owner, null, collection.iterator().next()); } if (!dropCommand.canExecute() || !analyzeForNonContainment(dropCommand)) { dropCommand.dispose(); dropCommand = AddCommand.create(domain, owner, null, collection); if (!analyzeForNonContainment(dropCommand)) { dropCommand.dispose(); dropCommand = UnexecutableCommand.INSTANCE; } } boolean result = dropCommand.canExecute(); return result; } protected boolean optimizedCanExecute() { // We'll assume that the copy command can execute and that adding a copy of the clipboard // is the same test as adding the clipboard contents itself. // Command addCommand = AddCommand.create(domain, owner, null, collection); boolean result = addCommand.canExecute() && !analyzeForNonContainment(addCommand); addCommand.dispose(); return result; } /** * This restores the command to its default initialized state, disposing an command that may have been contained. */ protected void reset() { if (dragCommand != null) { if (isDragCommandExecuted) { if (dragCommand.canUndo()) { dragCommand.undo(); } } dragCommand.dispose(); } if (dropCommand != null) { dropCommand.dispose(); } // Reset as in the constructor. // isPrepared = false; isExecutable = false; dragCommand = null; isDragCommandExecuted = false; dropCommand = null; optimizedDropCommandOwner = null; } /** * This is called by EditingDomainViewerDropAdapter to determine if the drag and drop operation is still enabled. */ public boolean validate(Object owner, float location, int operations, int operation, Collection<?> collection) { // If the operation has changed significantly... // if (owner != this.owner || location != this.location || (location < lowerLocationBound || location > upperLocationBound) || operation != this.operation || collection != this.collection) { // Clean it up. // reset(); // Set the arguments again. // this.owner = owner; this.location = location; this.operations = operations; this.operation = operation; this.collection = collection; // Determine if the operation is executable. // return canExecute(); } else { // Just return the cached result. // return isExecutable; } } public int getFeedback() { // Only return the feedback for an executable command. // return isExecutable ? feedback : FEEDBACK_SELECT; } public int getOperation() { // Only return the operation for an executable command. // return isExecutable ? operation : DROP_NONE; } public void execute() { if (dragCommand != null && !isDragCommandExecuted) { // special case in optimized mode, the drag command wasn't executed, so drag command was not yet created //if ((optimizedDropCommandOwner != null) && (dropCommand == null)) { if (optimizedDropCommandOwner != null) { if (dragCommand.canExecute()) { dragCommand.execute(); dropCommand = AddCommand.create(domain, optimizedDropCommandOwner, null, dragCommand.getResult()); } else { // Thread.dumpStack(); } } else { dragCommand.execute(); } } if (dropCommand != null) { if (dropCommand.canExecute()) { dropCommand.execute(); } else { // Thread.dumpStack(); } } } @Override public void undo() { if (dropCommand != null) { dropCommand.undo(); } if (dragCommand != null) { dragCommand.undo(); } } public void redo() { if (dragCommand != null) { dragCommand.redo(); } if (dropCommand != null) { dropCommand.redo(); } } @Override public void dispose() { if (dragCommand != null) { dragCommand.dispose(); } if (dropCommand != null) { dropCommand.dispose(); } } @Override public Collection<?> getResult() { return dropCommand != null ? dropCommand.getResult() : super.getResult(); } @Override public Collection<?> getAffectedObjects() { return dropCommand != null ? dropCommand.getAffectedObjects() : super.getAffectedObjects(); } /** * This gives an abbreviated name using this object's own class' name, without package qualification, * followed by a space separated list of <tt>field:value</tt> pairs. */ @Override public String toString() { StringBuffer result = new StringBuffer(super.toString()); result.append(" (domain: " + domain + ")"); result.append(" (owner: " + owner + ")"); result.append(" (location: " + location + ")"); result.append(" (lowerLocationBound: " + lowerLocationBound + ")"); result.append(" (upperLocationBound: " + upperLocationBound + ")"); result.append(" (operations: " + operations + ")"); result.append(" (operation: " + operation + ")"); result.append(" (collection: " + collection + ")"); result.append(" (feedback: " + feedback + ")"); return result.toString(); } }