/** * <copyright> * * Copyright (c) 2002, 2009 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 * * </copyright> * * $Id: DragAndDropCommand.java,v 1.10 2007/06/14 18:32:42 emerks Exp $ */ package net.enilink.komma.edit.command; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.ListIterator; import net.enilink.komma.common.command.AbstractCommand; import net.enilink.komma.common.command.CommandResult; import net.enilink.komma.common.command.CommandWrapper; import net.enilink.komma.common.command.CompositeCommand; import net.enilink.komma.common.command.ExtendedCompositeCommand; import net.enilink.komma.common.command.ICommand; import net.enilink.komma.common.command.IdentityCommand; import net.enilink.komma.common.command.UnexecutableCommand; import net.enilink.komma.common.util.Log; import net.enilink.komma.core.IReference; import net.enilink.komma.edit.KommaEditPlugin; import net.enilink.komma.edit.domain.IEditingDomain; import net.enilink.komma.em.concepts.IProperty; import net.enilink.komma.em.concepts.IResource; import net.enilink.komma.model.IModelAware; import net.enilink.komma.model.IModelSet; import org.eclipse.core.commands.ExecutionException; import org.eclipse.core.runtime.IAdaptable; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Status; /** * 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 IEditingDomain#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 IDragAndDropFeedback { /** * 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 operation; public int operations; public Detail(float location, int operations, int operation) { this.location = location; this.operations = operations; this.operation = operation; } } /** * This caches the description. */ protected static final String DESCRIPTION = KommaEditPlugin.INSTANCE .getString("_UI_DragAndDropCommand_description"); /** * This caches the label. */ protected static final String LABEL = KommaEditPlugin.INSTANCE .getString("_UI_DragAndDropCommand_label"); /** * 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 ICommand create(IEditingDomain 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 keeps track of the collection of dragged sources. */ protected Collection<?> collection; /** * This keeps track of the domain in which this command is created. */ protected IEditingDomain domain; /** * This keeps track of the command that implements the drag side of the * operation. */ protected ICommand dragCommand; /** * This keeps track of the command that implements the drop side of the * operation. */ protected ICommand dropCommand; /** * This keeps track of the feedback that will be returned by * {@link #getFeedback}. */ protected int feedback; /** * This keeps track of whether execute has been called on the * {@link #dragCommand}. */ protected boolean isDragCommandExecuted; /** * 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 current operation that will be returned by * {@link #getOperation}. */ protected int operation; /** * This keeps track of the permitted operations. */ protected int operations; /** * This keeps track of the owner that is the target of the drag and drop. */ protected Object owner; /** * This keeps track of the upper range of locations in which the effect of * this command remains unchanged. */ protected float upperLocationBound; /** * 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(IEditingDomain domain, Object owner, float location, int operations, int operation, Collection<?> collection) { super(LABEL, DESCRIPTION); this.domain = domain; this.owner = owner; this.location = location; this.operations = operations; this.operation = operation; this.collection = collection; } protected boolean analyzeForNonContainment(ICommand command) { if (command instanceof AddCommand) { return isNonContainment(((AddCommand) command).getProperty()); } else if (command instanceof SetCommand) { return isNonContainment(((SetCommand) command).getProperty()); } else if (command instanceof CommandWrapper) { return analyzeForNonContainment(((CommandWrapper) command) .getCommand()); } else if (command instanceof ExtendedCompositeCommand) { for (ICommand childCommand : ((ExtendedCompositeCommand) command) .getCommandList()) { if (analyzeForNonContainment(childCommand)) { return true; } } } return false; } @Override public void dispose() { if (dragCommand != null) { dragCommand.dispose(); } if (dropCommand != null) { dropCommand.dispose(); } } @Override protected CommandResult doExecuteWithResult( IProgressMonitor progressMonitor, IAdaptable info) throws ExecutionException { if (dragCommand != null && !isDragCommandExecuted) { if (!dragCommand.execute(progressMonitor, info).isOK()) { return dragCommand.getCommandResult(); } } if (dropCommand != null) { if (dropCommand.canExecute()) { dropCommand.execute(progressMonitor, info); return dropCommand.getCommandResult(); } else { // Thread.dumpStack(); } } return CommandResult.newOKCommandResult(); } @Override protected CommandResult doRedoWithResult(IProgressMonitor progressMonitor, IAdaptable info) throws ExecutionException { if (dragCommand != null) { if (dragCommand.redo(progressMonitor, info).isOK()) { return dragCommand.getCommandResult(); } } if (dropCommand != null) { if (dropCommand.redo(progressMonitor, info).isOK()) { return dropCommand.getCommandResult(); } } return CommandResult.newOKCommandResult(); } @Override protected CommandResult doUndoWithResult(IProgressMonitor progressMonitor, IAdaptable info) throws ExecutionException { if (dropCommand != null) { if (dropCommand.undo(progressMonitor, info).isOK()) { return dropCommand.getCommandResult(); } } if (dragCommand != null) { if (dragCommand.undo(progressMonitor, info).isOK()) { return dragCommand.getCommandResult(); } } return CommandResult.newOKCommandResult(); } @Override public Collection<?> getAffectedObjects() { return dropCommand != null ? dropCommand.getAffectedObjects() : super .getAffectedObjects(); } @Override public Collection<?> getAffectedResources(Object type) { Collection<Object> affected = new HashSet<Object>(); if (dragCommand != null) { affected.addAll(dragCommand.getAffectedResources(type)); } if (dropCommand != null) { affected.addAll(dropCommand.getAffectedResources(type)); } return affected; } /** * This can be overridden to determine the children of an object; this * implementation uses {@link IEditingDomain#getChildren}. */ protected Collection<?> getChildren(Object object) { return domain.getChildren(object); } public Collection<?> getCollection() { return collection; } public int getFeedback() { // Only return the feedback for an executable command. // return isExecutable ? feedback : FEEDBACK_SELECT; } public float getLocation() { return location; } public int getOperation() { // Only return the operation for an executable command. // return isExecutable ? operation : DROP_NONE; } public int getOperations() { return operations; } public Object getOwner() { return owner; } /** * This can be overridden to determine the parent of an object; this * implementation uses {@link IEditingDomain#getParent}. */ protected Object getParent(Object object) { return domain.getParent(object); } protected boolean isCrossModelSet() { for (Object item : collection) { if (item instanceof IModelAware) { IModelSet itemModelSet = ((IModelAware) item).getModel() .getModelSet(); if (itemModelSet != null && !itemModelSet.equals(domain.getModelSet())) { return true; } } } return false; } protected boolean isNonContainment(IReference property) { property = ((IResource) owner).getEntityManager().find(property); return !((IProperty) property).isContainment(); } /** * 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 attempts to prepare a drop copy insert operation. */ protected boolean prepareDropCopyInsert(final Object parent, Collection<?> children, final int index) { // We don't want to copy insert an object before or after itself... if (collection.contains(owner)) { dragCommand = IdentityCommand.INSTANCE; dropCommand = UnexecutableCommand.INSTANCE; } else { // do nothing on drag dragCommand = IdentityCommand.INSTANCE; // Copy the collection final ICommand copyCommand = CopyCommand.create(domain, collection, null); CompositeCommand copyAndAddCommand = new CompositeCommand() { @Override protected CommandResult doExecuteWithResult( IProgressMonitor progressMonitor, IAdaptable info) throws ExecutionException { CommandResult result = super.doExecuteWithResult( progressMonitor, info); if (!result.getStatus().isOK()) { return result; } IStatus status = addAndExecute(AddCommand.create(domain, owner, null, copyCommand.getCommandResult() .getReturnValues()), progressMonitor, info); return CommandResult.newCommandResult(status, result.getReturnValue()); } }; copyAndAddCommand.add(copyCommand); if (copyAndAddCommand.canExecute() && copyAndAddCommand.canUndo()) { ICommand dummyAdd = AddCommand.create(domain, owner, null, collection); if (!dummyAdd.canExecute() || analyzeForNonContainment(dummyAdd)) { dropCommand = UnexecutableCommand.INSTANCE; copyAndAddCommand.dispose(); } else { dropCommand = copyAndAddCommand; } dummyAdd.dispose(); } else { dropCommand = UnexecutableCommand.INSTANCE; } } // if collection return dragCommand.canExecute() && dropCommand.canExecute(); } /** * This attempts to prepare a drop copy on operation. */ protected boolean prepareDropCopyOn() { dragCommand = IdentityCommand.INSTANCE; final ICommand copyCommand = CopyCommand.create(domain, collection, null); CompositeCommand copyAndAddCommand = new CompositeCommand() { @Override protected CommandResult doExecuteWithResult( IProgressMonitor progressMonitor, IAdaptable info) throws ExecutionException { CommandResult result = super.doExecuteWithResult( progressMonitor, info); if (!result.getStatus().isOK()) { return result; } IStatus status = addAndExecute( AddCommand.create(domain, owner, null, copyCommand .getCommandResult().getReturnValues()), progressMonitor, info); return CommandResult.newCommandResult(status, result.getReturnValue()); } }; copyAndAddCommand.add(copyCommand); if (copyAndAddCommand.canExecute() && copyAndAddCommand.canUndo()) { ICommand dummyAdd = AddCommand.create(domain, owner, null, collection); if (!dummyAdd.canExecute() || analyzeForNonContainment(dummyAdd)) { dropCommand = UnexecutableCommand.INSTANCE; copyAndAddCommand.dispose(); } else { dropCommand = copyAndAddCommand; } dummyAdd.dispose(); } else { dropCommand = UnexecutableCommand.INSTANCE; } return dragCommand.canExecute() && dropCommand.canExecute(); } /** * 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.equals(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 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 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; } /** * 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. // ExtendedCompositeCommand compoundCommand = new ExtendedCompositeCommand(); 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.add(MoveCommand.create(domain, parent, null, object, index - 1)); } for (ListIterator<Object> objects = after .listIterator(after.size()); objects.hasPrevious();) { Object object = objects.previous(); compoundCommand.add(MoveCommand.create(domain, parent, null, object, index)); } dropCommand = compoundCommand.getCommandList().size() == 0 ? (ICommand) IdentityCommand.INSTANCE : compoundCommand; } else if (isCrossModelSet()) { dragCommand = IdentityCommand.INSTANCE; dropCommand = UnexecutableCommand.INSTANCE; } else { // Just remove the objects and add them. dropCommand = AddCommand.create(domain, parent, null, collection, index); if (!dropCommand.canExecute() || 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 move on operation. */ protected boolean prepareDropMoveOn() { if (isCrossModelSet()) { 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 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 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()) { try { dragCommand.undo(null, null); } catch (ExecutionException e) { Log.log(KommaEditPlugin.getPlugin(), new Status( IStatus.ERROR, KommaEditPlugin.PLUGIN_ID, "Error while undoing drag command", e)); } } } dragCommand.dispose(); } if (dropCommand != null) { dropCommand.dispose(); } // Reset as in the constructor. // isPrepared = false; isExecutable = false; dragCommand = null; isDragCommandExecuted = false; dropCommand = null; } /** * 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(); } /** * 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; } } }