/*******************************************************************************
* Copyright (c) 2006-2013 The RCP Company 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:
* The RCP Company - initial API and implementation
*******************************************************************************/
package com.rcpcompany.uibindings.internal.utils.dnd;
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.CompoundCommand;
import org.eclipse.emf.common.command.IdentityCommand;
import org.eclipse.emf.common.command.UnexecutableCommand;
import org.eclipse.emf.ecore.EClass;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.EReference;
import org.eclipse.emf.edit.EMFEditPlugin;
import org.eclipse.emf.edit.command.AddCommand;
import org.eclipse.emf.edit.command.CopyCommand;
import org.eclipse.emf.edit.command.DragAndDropFeedback;
import org.eclipse.emf.edit.command.MoveCommand;
import org.eclipse.emf.edit.command.RemoveCommand;
import org.eclipse.emf.edit.domain.AdapterFactoryEditingDomain;
import org.eclipse.emf.edit.domain.EditingDomain;
import com.rcpcompany.uibindings.IChildCreationSpecification;
import com.rcpcompany.uibindings.IContainerBinding;
import com.rcpcompany.uibindings.IContainerBinding.IContainerDropContext;
import com.rcpcompany.uibindings.IManager;
/**
* Implementation of a drag 'n drop command.
* <p>
* The real work is delegated to a drag and drop command.
* <p>
* Based on {@link org.eclipse.emf.edit.command.DragAndDropCommand}.
*
* @author Tonny Madsen, The RCP Company
*
*/
public class ContainerDragAndDropCommand extends AbstractCommand implements DragAndDropFeedback {
/**
* 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 myDomain;
/**
* This keeps track of the lower range of locations in which the effect of this command remains
* unchanged.
*/
protected float myLowerLocationBound;
/**
* This keeps track of the upper range of locations in which the effect of this command remains
* unchanged.
*/
protected float myUpperLocationBound;
/**
* This keeps track of the permitted operations.
*/
protected int myOperations;
/**
* This keeps track of the current operation that will be returned by {@link #getOperation}.
*/
protected int myOperation;
/**
* 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<EObject> mySourceObjects;
/**
* This keeps track of the command that implements the drag side of the operation.
*/
protected Command myDragCommand = IdentityCommand.INSTANCE;
public Command getDragCommand() {
return myDragCommand;
}
public Command getDropCommand() {
return myDropCommand;
}
/**
* This keeps track of the command that implements the drop side of the operation.
*/
protected Command myDropCommand = UnexecutableCommand.INSTANCE;
/**
* The container binding for the target.
*/
private final IContainerBinding myContainer;
private boolean isDragCommandExecuted;
private IContainerDropContext myContext;
/**
* Creates an 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.
*
* @param binding
* @param target
* @param location
* @param operations
* @param operation
* @param source
*/
public ContainerDragAndDropCommand(IContainerBinding container, IContainerDropContext context, int operations,
int operation, Collection<EObject> source) {
super(LABEL, DESCRIPTION);
myContainer = container;
myContext = context;
myDomain = myContainer.getEditingDomain();
myOperations = operations;
myOperation = operation;
mySourceObjects = source;
}
/**
* 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() {
boolean result = false;
/*
* If there isn't something obviously wrong with the arguments...
*/
if (myContext.getDropTargetObject() == null || mySourceObjects == null || myOperations == DROP_NONE
|| myOperation == DROP_NONE) {
myLowerLocationBound = 0.0F;
myUpperLocationBound = 1.0F;
return false;
}
/*
* If the location is near the boundary, we'll start by trying to do a drop insert.
*/
if (myContext.getDropLocation() <= 0.20 || myContext.getDropLocation() >= 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 (myContext.getDropLocation() <= 0.20) {
myLowerLocationBound = 0.0F;
myUpperLocationBound = 0.2F;
} else {
myLowerLocationBound = 0.8F;
myUpperLocationBound = 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 (myContext.getDropLocation() <= 0.20) {
myLowerLocationBound = 0.0F;
myUpperLocationBound = 0.8F;
} else {
myLowerLocationBound = 0.2F;
myUpperLocationBound = 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.
*/
myLowerLocationBound = 0.2F;
myUpperLocationBound = 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 (myContext.getDropLocation() <= 0.50) {
myLowerLocationBound = 0.0F;
myUpperLocationBound = 0.5F;
} else {
myLowerLocationBound = 0.5F;
myUpperLocationBound = 1.0F;
}
}
}
// LogUtils.debug(this, "result=" + result + "\ndrag=" +
// EcoreExtUtils.toString(myDragCommand) + "\ndrop="
// + EcoreExtUtils.toString(myDropCommand));
return result;
}
/**
* Finds the best possible {@link IChildCreationSpecification} for the parent and sibling given
* the current collection of dragged objects.
* <p>
* Exactly one of parent and sibling must be non-<code>null</code>.
*
* @param parent the parent object or <code>null</code>
* @param sibling the sibling object or <code>null</code>
* @return the best specification or <code>null</code>
*/
private IChildCreationSpecification findBestChildCreationSpecification(EObject parent, EObject sibling) {
final List<IChildCreationSpecification> possibleChildObjects = myContext.getPossibleChildObjects(parent,
sibling);
if (possibleChildObjects == null) return null;
/*
* See, if we can find a direct match on the type (remember that the list already contains
* all known sub-types, so we need not use instanceof)...
*/
OUTER: for (final IChildCreationSpecification pcs : possibleChildObjects) {
final EClass childType = pcs.getChildType();
if (childType == null) {
continue;
}
for (final EObject source : mySourceObjects) {
if (source.eClass() != childType) {
continue OUTER;
}
}
return pcs;
}
/*
* No match.
*
* Check if we can find a converter to a supported child type...
*
* TODO
*/
return null;
}
/**
* This attempts to prepare a drop insert operation.
*/
protected boolean prepareDropInsert() {
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 = myContext.getDropLocation() < 0.5 ? FEEDBACK_INSERT_BEFORE : FEEDBACK_INSERT_AFTER;
final IChildCreationSpecification spec = findBestChildCreationSpecification(null,
myContext.getDropTargetObject());
if (spec == null) return false;
/*
* If we can't determine the parent.
*/
int index = spec.getIndex();
/*
* If the location indicates after, add one more to get the correct index.
*/
if (myContext.getDropLocation() >= 0.5 && index != -1) {
++index;
}
/*
* Try to create a specific command based on the current desired operation.
*/
switch (myOperation) {
case DROP_MOVE:
result = prepareDropMoveInsert(spec, index);
break;
case DROP_COPY:
result = prepareDropCopyInsert(spec, index);
break;
case DROP_LINK:
result = prepareDropLinkInsert(spec, index);
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 && myOperation != DROP_COPY && myOperation != DROP_LINK && (myOperations & DROP_COPY) != 0) {
reset();
result = prepareDropCopyInsert(spec, index);
if (result) {
/*
* We've switch the operation!
*/
myOperation = 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 && myOperation != DROP_LINK && (myOperations & DROP_LINK) != 0) {
reset();
result = prepareDropLinkInsert(spec, index);
if (result) {
/*
* We've switch the operation!
*/
myOperation = DROP_LINK;
}
}
return result;
}
/**
* This attempts to prepare a drop move insert operation.
*/
protected boolean prepareDropMoveInsert(IChildCreationSpecification spec, int index) {
if (isCrossDomain()) return false;
/*
* We don't want to move insert an object before or after itself...
*/
if (mySourceObjects.contains(myContext.getDropTargetObject())) return false;
/*
* We need containment to move
*/
if (!spec.getReference().isContainment()) return false;
/*
* If the dragged objects are already present in the list, then move them in the list rather
* than adding them to the new list...
*/
final Object o = spec.getParent().eGet(spec.getReference());
if (o instanceof List<?>) {
final List<?> children = (List<?>) o;
if (children.containsAll(mySourceObjects)) {
/*
* Create move commands for all the objects in the collection.
*/
final CompoundCommand compoundCommand = new CompoundCommand();
final List<Object> before = new ArrayList<Object>();
final List<Object> after = new ArrayList<Object>();
int j = 0;
for (final Object object : children) {
if (mySourceObjects.contains(object)) {
if (j < index) {
before.add(object);
} else if (j > index) {
after.add(object);
}
}
++j;
}
for (final Object object : before) {
compoundCommand.append(MoveCommand.create(myDomain, spec.getParent(), spec.getReference(), object,
index - 1));
}
for (final ListIterator<Object> objects = after.listIterator(after.size()); objects.hasPrevious();) {
final Object object = objects.previous();
compoundCommand.append(MoveCommand.create(myDomain, spec.getParent(), spec.getReference(), object,
index));
}
switch (compoundCommand.getCommandList().size()) {
case 0:
myDropCommand = IdentityCommand.INSTANCE;
break;
case 1:
myDropCommand = compoundCommand.unwrap();
break;
default:
myDropCommand = compoundCommand;
break;
}
return myDragCommand.canExecute() && myDropCommand.canExecute();
}
}
/*
* Just remove the objects and add them.
*/
myDragCommand = RemoveCommand.create(myDomain, mySourceObjects);
myDropCommand = AddCommand.create(myDomain, spec.getParent(), spec.getReference(), mySourceObjects, index);
return myDragCommand.canExecute() && myDropCommand.canExecute();
}
protected boolean isCrossDomain() {
for (final EObject item : mySourceObjects) {
final EditingDomain itemDomain = AdapterFactoryEditingDomain.getEditingDomainFor(item);
if (itemDomain != null && itemDomain != myDomain) return true;
}
return false;
}
/**
* This attempts to prepare a drop copy insert operation.
*/
protected boolean prepareDropCopyInsert(final IChildCreationSpecification spec, final int index) {
/*
* We need containment to copy
*/
if (!spec.getReference().isContainment()) return false;
if (isCopyOnSameParent(spec)) return false;
/*
* We don't want to copy insert an object before or after itself...
*/
if (mySourceObjects.contains(spec.getParent())) return false;
/*
* Copy the collection
*/
myDragCommand = CopyCommand.create(myDomain, mySourceObjects);
if (myDragCommand.canExecute() && myDragCommand.canUndo()) {
myDragCommand.execute();
isDragCommandExecuted = true;
myDropCommand = AddCommand.create(myDomain, spec.getParent(), spec.getReference(),
myDragCommand.getResult(), index);
}
return myDragCommand.canExecute() && myDropCommand.canExecute();
}
/**
* This attempts to prepare a drop link insert operation.
*/
protected boolean prepareDropLinkInsert(IChildCreationSpecification spec, int index) {
/*
* We need NON-containment to link
*/
final EReference ref = spec.getReference();
if (ref.isContainment()) return false;
/*
* Also, if this is a to-many reference with an opposite, then it cannot be a link
*/
if (ref.isMany() && ref.getEOpposite() != null) return false;
/*
* We don't want to insert an object before or after itself...
*/
if (mySourceObjects.contains(spec.getParent())) return false;
myDropCommand = AddCommand.create(myDomain, spec.getParent(), ref, mySourceObjects, index);
return myDragCommand.canExecute() && myDropCommand.canExecute();
}
/**
* This attempts to prepare a drop on operation.
*/
protected boolean prepareDropOn() {
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;
final EObject targetObject = myContext.getDropTargetObject();
final IChildCreationSpecification spec = findBestChildCreationSpecification(targetObject, null);
if (spec == null) {
// LogUtils.debug(this, "No specs, trying assignment");
/*
* No matching specs...
*
* See if we can find an assignment instead
*/
if (mySourceObjects.size() != 1) return false;
final EObject obj = mySourceObjects.iterator().next();
final Command assignCommand = IManager.Factory.getManager().assignObject(myDomain, myContainer,
targetObject, obj);
if (assignCommand == null) return false;
myDropCommand = assignCommand;
return true;
}
/*
* Check whether we are dropping on one of the source objects
*/
if (mySourceObjects.contains(myContext.getDropTargetObject())) return false;
/*
* Prepare the right type of operation.
*/
switch (myOperation) {
case DROP_MOVE:
result = prepareDropMoveOn(spec);
break;
case DROP_COPY:
result = prepareDropCopyOn(spec);
break;
case DROP_LINK:
result = prepareDropLinkOn(spec);
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 && myOperation != DROP_COPY && myOperation != DROP_LINK && (myOperations & DROP_COPY) != 0) {
reset();
result = prepareDropCopyOn(spec);
if (result) {
myOperation = 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 && myOperation != DROP_LINK && (myOperations & DROP_LINK) != 0) {
reset();
result = prepareDropLinkOn(spec);
if (result) {
myOperation = DROP_LINK;
}
}
return result;
}
/**
* This attempts to prepare a drop move on operation.
*
* @param spec
*/
protected boolean prepareDropMoveOn(IChildCreationSpecification spec) {
if (isCrossDomain()) return false;
/*
* We need containment to move
*/
if (!spec.getReference().isContainment()) return false;
myDragCommand = RemoveCommand.create(myDomain, mySourceObjects);
myDropCommand = AddCommand.create(myDomain, spec.getParent(), spec.getReference(), mySourceObjects);
return myDragCommand.canExecute() && myDropCommand.canExecute();
}
/**
* This attempts to prepare a drop copy on operation.
*
* @param spec
*/
protected boolean prepareDropCopyOn(IChildCreationSpecification spec) {
/*
* We need containment to copy
*/
if (!spec.getReference().isContainment()) return false;
if (isCopyOnSameParent(spec)) return false;
myDragCommand = CopyCommand.create(myDomain, mySourceObjects);
if (myDragCommand.canExecute() && myDragCommand.canUndo()) {
myDragCommand.execute();
isDragCommandExecuted = true;
myDropCommand = AddCommand.create(myDomain, spec.getParent(), spec.getReference(),
myDragCommand.getResult());
}
return myDragCommand.canExecute() && myDropCommand.canExecute();
}
/**
* Only allow a COPY on the same parent, when explicitly requested.
*
* @return <code>true</code> if the copy operation should not be allowed
*/
private boolean isCopyOnSameParent(IChildCreationSpecification spec) {
if (myOperation != DROP_COPY) {
for (final EObject so : mySourceObjects) {
if (so.eContainer() == spec.getParent()) return true;
}
}
return false;
}
/**
* This attempts to prepare a drop link on operation.
*
* @param spec
*/
protected boolean prepareDropLinkOn(IChildCreationSpecification spec) {
/*
* We need NON-containment to copy
*/
final EReference ref = spec.getReference();
if (ref.isContainment()) return false;
/*
* Also, if this is a to-many reference with an opposite, then it cannot be a link
*/
if (ref.isMany() && ref.getEOpposite() != null) return false;
myDropCommand = AddCommand.create(myDomain, spec.getParent(), ref, mySourceObjects);
return myDropCommand.canExecute();
}
/**
* This restores the command to its default initialized state, disposing an command that may
* have been contained.
*/
protected void reset() {
if (isDragCommandExecuted) {
myDragCommand.undo();
isDragCommandExecuted = false;
}
myDragCommand.dispose();
myDropCommand.dispose();
isPrepared = false;
isExecutable = false;
myDragCommand = IdentityCommand.INSTANCE;
myDropCommand = UnexecutableCommand.INSTANCE;
}
@Override
public boolean validate(Object owner, float location, int operations, int operation, Collection<?> collection) {
throw new RuntimeException("Method not supported");
}
public boolean revalidate(IContainerDropContext context, int operations, int operation) {
/*
* If the operation has NOT changed significantly, then just return the cached result.
*/
if (context.getDropTargetObject() == myContext.getDropTargetObject()
&& (context.getDropLocation() >= myLowerLocationBound && context.getDropLocation() <= myUpperLocationBound)
&& operation == myOperation) return isExecutable;
reset();
myContext = context;
myOperations = operations;
myOperation = operation;
return canExecute();
}
@Override
public int getFeedback() {
return isExecutable ? feedback : FEEDBACK_SELECT;
}
@Override
public int getOperation() {
return isExecutable ? myOperation : DROP_NONE;
}
@Override
public void execute() {
// LogUtils.debug(this,
// "\ndrag=" + EcoreExtUtils.toString(myDragCommand) + "\ndrop=" +
// EcoreExtUtils.toString(myDropCommand));
if (myDropCommand.canExecute() && !isDragCommandExecuted) {
myDragCommand.execute();
}
isDragCommandExecuted = true;
if (myDropCommand.canExecute()) {
myDropCommand.execute();
}
}
@Override
public void undo() {
/*
* Reverse sequence...
*/
myDropCommand.undo();
myDragCommand.undo();
}
@Override
public void redo() {
myDragCommand.redo();
myDropCommand.redo();
}
@Override
public void dispose() {
reset();
}
@Override
public Collection<?> getResult() {
return myDropCommand.getResult();
}
@Override
public Collection<?> getAffectedObjects() {
return myDropCommand.getAffectedObjects();
}
@Override
public String toString() {
final StringBuffer result = new StringBuffer(super.toString());
result.append(" (domain: " + myDomain + ")");
result.append(" (owner: " + myContext.getDropTargetObject() + ")");
result.append(" (location: " + myContext.getDropLocation() + ")");
result.append(" (lowerLocationBound: " + myLowerLocationBound + ")");
result.append(" (upperLocationBound: " + myUpperLocationBound + ")");
result.append(" (operations: " + myOperations + ")");
result.append(" (operation: " + myOperation + ")");
result.append(" (collection: " + mySourceObjects + ")");
result.append(" (feedback: " + feedback + ")");
return result.toString();
}
}