/** * <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: EditingDomainViewerDropAdapter.java,v 1.10 2008/07/09 00:56:41 davidms Exp $ */ package net.enilink.komma.edit.ui.dnd; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; import org.eclipse.core.commands.ExecutionException; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.NullProgressMonitor; import org.eclipse.core.runtime.Status; import org.eclipse.jface.viewers.IStructuredSelection; import org.eclipse.jface.viewers.Viewer; import org.eclipse.swt.SWT; import org.eclipse.swt.dnd.DND; import org.eclipse.swt.dnd.DropTargetAdapter; import org.eclipse.swt.dnd.DropTargetEvent; import org.eclipse.swt.dnd.TransferData; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.graphics.Rectangle; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.TableItem; import org.eclipse.swt.widgets.TreeItem; import org.eclipse.swt.widgets.Widget; import net.enilink.komma.common.command.ICommand; import net.enilink.komma.common.util.Log; import net.enilink.komma.edit.command.DragAndDropCommand; import net.enilink.komma.edit.command.IDragAndDropFeedback; import net.enilink.komma.edit.domain.IEditingDomain; import net.enilink.komma.edit.ui.KommaEditUIPlugin; import net.enilink.komma.edit.ui.internal.EditUIStatusCodes; /** * This implementation of a drop target listener is designed to turn a drag and * drop operation into a {@link ICommand} based on the model objects of an * {@link EditingDomain} and created by {@link DragAndDropCommand#create}. It is * designed to do early data transfer so the the enablement and feedback of the * drag and drop interaction can intimately depend on the state of the model * objects involved. On some platforms, however, early data transfer is not * available, so this feedback cannot be provided. * <p> * The base implementation of this class should be sufficient for most * applications. Any change in behaviour is typically accomplished by overriding * {@link org.eclipse.emf.edit.provider.ItemProviderAdapter} * .createDragAndDropCommand to return a derived implementation of * {@link DragAndDropCommand}. This is how one these adapters is typically * hooked up: * * <pre> * viewer.addDropSupport(DND.DROP_COPY | DND.DROP_MOVE | DND.DROP_LINK, * new Transfer[] { LocalTransfer.getInstance() }, * EditingDomainViewerDropAdapter(viewer)); * </pre> * <p> * This implementation prefers to use a {@link LocalTransfer}, which * short-circuits the transfer process for simple transfers within the * workbench, the method {@link #getDragSource} can be overridden to change the * behaviour. The implementation also only handles an * {@link IStructuredSelection}, but the method {@link #extractDragSource} can * be overridden to change the behaviour. * <p> * SWT's {@link DND#FEEDBACK_SCROLL auto-scroll} and {@link DND#FEEDBACK_EXPAND * auto-expand} (hover) are enabled by default. The method * {@link #getAutoFeedback} can be overridden to change this behaviour. */ public class EditingDomainViewerDropAdapter extends DropTargetAdapter { /** * This indicates whether the current platform is motif, which needs special * treatment, since it cannot do early data transfer, but doesn't cleanly * return null either. */ protected final static boolean IS_MOTIF = "motif".equals(SWT.getPlatform()); /** * This is the viewer for which this is a drop target listener. */ protected Viewer viewer; /** * This is the domain in which drag and drop commands will be executed. */ protected WeakReference<IEditingDomain> domainReference; /** * This is the collection of source objects being dragged. */ protected Collection<?> source; /** * This is the command created during dragging which provides the feedback * and will carry out the action upon completion. */ protected ICommand command; /** * This records the object for which the {@link #command} was created. */ protected Object commandTarget; /** * This keeps track of the original operation that the user requested, * before we started changing the event.detail. We always try to create the * command using this. */ protected int originalOperation; /** * This keeps track of the information used to create {@link #command}, but * does not need to be disposed. This allows us to dispose of the command in * dragLeave, and then, if we need to execute it, recreate it in drop. */ protected DragAndDropCommandInformation dragAndDropCommandInformation; /** * This creates an instance with the given domain and viewer. */ public EditingDomainViewerDropAdapter(IEditingDomain domain, Viewer viewer) { this.viewer = viewer; this.domainReference = new WeakReference<IEditingDomain>(domain); } /** * This is called when the mouse first enters or starts dragging in the * viewer. */ @Override public void dragEnter(DropTargetEvent event) { // Remember the requested operation. originalOperation = event.detail; helper(event); } /** * This is called when the mouse leaves or stops dragging in the viewer, * whether the operation was aborted or is about to do a dropAccept and * drop. The event argument is uninitialized, so it is impossible to * distinguish between the two cases. So, we do the clean-up now and * recreate the command later, if necessary. */ @Override public void dragLeave(DropTargetEvent event) { // Clean up the command if there is one. If we need it again in drop, // we'll recreate it from dragAndDropCommandInformation. // if (command != null) { command.dispose(); command = null; commandTarget = null; } // Clear the source data. We won't need this again, since, if it was // available, it's already in the command. // source = null; } /** * This is called when the operation has changed in some way, typically * because the user changes keyboard modifiers. */ @Override public void dragOperationChanged(DropTargetEvent event) { // Remember the requested operation. originalOperation = event.detail; helper(event); } /** * This is called repeatedly, as the mouse moves over the viewer. */ @Override public void dragOver(DropTargetEvent event) { helper(event); } /** * This is called when the mouse is released over the viewer to initiate a * drop, between dragLeave and drop. */ @Override public void dropAccept(DropTargetEvent event) { helper(event); } /** * This is called to indicate that the drop action should be invoked. */ @Override public void drop(DropTargetEvent event) { IEditingDomain domain = domainReference.get(); if (domain != null) { // A command was created if the source was available early, and the // information used to create it was cached... // if (dragAndDropCommandInformation != null) { // Recreate the command. // command = dragAndDropCommandInformation.createCommand(); } else { // Otherwise, the source should be available now as event.data, // and // we // can create the command. // source = extractDragSource(event.data); Object target = extractDropTarget(event.item); command = DragAndDropCommand.create(domain, target, getLocation(event), event.operations, originalOperation, source); } // If the command can execute... // if (command.canExecute()) { // Execute it. // try { domain.getCommandStack().execute(command, new NullProgressMonitor(), null); } catch (ExecutionException e) { Log.log(KommaEditUIPlugin.getPlugin(), new Status(IStatus.ERROR, KommaEditUIPlugin.PLUGIN_ID, EditUIStatusCodes.ACTION_FAILURE, String .valueOf(e.getMessage()), e)); } } else { // Otherwise, let's call the whole thing off. // event.detail = DND.DROP_NONE; command.dispose(); } } else { event.detail = DND.DROP_NONE; } // Clean up the state. // command = null; commandTarget = null; source = null; dragAndDropCommandInformation = null; } /** * This method is called the same way for each of the * {@link org.eclipse.swt.dnd.DropTargetListener} methods, except for leave * and drop. If the source is available early, it creates or revalidates the * {@link DragAndDropCommand}, and updates the event's detail (operation) * and feedback (drag under effect), appropriately. */ protected void helper(DropTargetEvent event) { // If we can't do anything else, we'll provide the default select // feedback // and enable auto-scroll and auto-expand effects. event.feedback = DND.FEEDBACK_SELECT | getAutoFeedback(); // If we don't already have it, try to get the source early. We can't // give // feedback if it's not available yet (this is platform-dependent). // if (source == null) { source = getDragSource(event); if (source == null) { // Clear out any old information from a previous drag. // dragAndDropCommandInformation = null; return; } } // Get the target object from the item widget and the mouse location in // it. // Object target = extractDropTarget(event.item); float location = getLocation(event); // Determine if we can create a valid command at the current location. // boolean valid = false; IEditingDomain domain = domainReference.get(); if (domain == null) { return; } // If we don't have a previous cached command... if (command == null) { // We'll need to keep track of the information we use to create the // command, so that we can recreate it in drop. dragAndDropCommandInformation = new DragAndDropCommandInformation( domain, target, location, event.operations, originalOperation, source); // Remember the target; create the command and test if it is // executable. // commandTarget = target; command = dragAndDropCommandInformation.createCommand(); valid = command.canExecute(); } else { // Check if the cached command can provide DND // feedback/revalidation. // if (target == commandTarget && command instanceof IDragAndDropFeedback) { // If so, revalidate the command. // valid = ((IDragAndDropFeedback) command).validate(target, location, event.operations, originalOperation, source); // Keep track of any changes to the command information. dragAndDropCommandInformation = new DragAndDropCommandInformation( domain, target, location, event.operations, originalOperation, source); } else { // If not, dispose the current command and create a new one. // dragAndDropCommandInformation = new DragAndDropCommandInformation( domain, target, location, event.operations, originalOperation, source); commandTarget = target; command.dispose(); command = dragAndDropCommandInformation.createCommand(); valid = command.canExecute(); } } // If this command can provide detailed drag and drop feedback... // if (command instanceof IDragAndDropFeedback) { // Use it for the operation and drag under effect. // IDragAndDropFeedback dragAndDropFeedback = (IDragAndDropFeedback) command; event.detail = dragAndDropFeedback.getOperation(); event.feedback = dragAndDropFeedback.getFeedback() | getAutoFeedback(); } else if (!valid) { // There is no executable command, so we'd better nix the whole // deal. // event.detail = DND.DROP_NONE; } } /** * This returns the bitwise OR'ed flags for desired auto-feedback effects. * Drag under effect DND constants are always OR'ed with this to enable * them. This implementation enables {@link DND#FEEDBACK_SCROLL auto-scroll} * and {@link DND#FEEDBACK_EXPAND auto-expand} (hover). */ protected int getAutoFeedback() { return DND.FEEDBACK_SCROLL | DND.FEEDBACK_EXPAND; } /** * This attempts to extract the drag source from the event early, i.e., * before the drop method. This implementation tries to use a * {@link net.enilink.komma.edit.ui.dnd.LocalTransfer}. If the data is * not yet available (e.g. on platforms other than win32), it just returns * null. */ protected Collection<?> getDragSource(DropTargetEvent event) { // Check whether the current data type can be transfered locally. // LocalTransfer localTransfer = LocalTransfer.getInstance(); if (!localTransfer.isSupportedType(event.currentDataType)) { // Iterate over the data types to see if there is a data type that // supports a local transfer. // TransferData[] dataTypes = event.dataTypes; for (int i = 0; i < dataTypes.length; ++i) { TransferData transferData = dataTypes[i]; // If the local transfer supports this data type, switch to that // data type // if (localTransfer.isSupportedType(transferData)) { event.currentDataType = transferData; } } return null; } else { // Motif kludge: we would get something random instead of null. // if (IS_MOTIF) return null; // Transfer the data and, if non-null, extract it. // Object object = localTransfer.nativeToJava(event.currentDataType); return object == null ? null : extractDragSource(object); } } /** * This extracts a collection of dragged source objects from the given * object retrieved from the transfer agent. This default implementation * converts a structured selection into a collection of elements. */ protected Collection<?> extractDragSource(Object object) { // Transfer the data and convert the structured selection to a // collection of objects. // if (object instanceof IStructuredSelection) { List<?> list = ((IStructuredSelection) object).toList(); return list; } else { return Collections.EMPTY_LIST; } } /** * This extracts an object from the given item widget. */ protected Object extractDropTarget(Widget item) { if (item == null) return null; return item.getData(); } /** * This returns the location of the mouse in the vertical direction, * relative to the item widget, from 0 (top) to 1 (bottom). */ protected float getLocation(DropTargetEvent event) { if (event.item instanceof TreeItem) { TreeItem treeItem = (TreeItem) event.item; Control control = treeItem.getParent(); Point point = control.toControl(new Point(event.x, event.y)); Rectangle bounds = treeItem.getBounds(); return (float) (point.y - bounds.y) / (float) bounds.height; } else if (event.item instanceof TableItem) { TableItem tableItem = (TableItem) event.item; Control control = tableItem.getParent(); Point point = control.toControl(new Point(event.x, event.y)); Rectangle bounds = tableItem.getBounds(0); return (float) (point.y - bounds.y) / (float) bounds.height; } else { return 0.0F; } } /** * This holds all of the information used to create a * {@link DragAndDropCommand}, but does not need to be disposed. */ protected static class DragAndDropCommandInformation { protected IEditingDomain domain; protected Object target; protected float location; protected int operations; protected int operation; protected Collection<?> source; public DragAndDropCommandInformation(IEditingDomain domain, Object target, float location, int operations, int operation, Collection<?> source) { this.domain = domain; this.target = target; this.location = location; this.operations = operations; this.operation = operation; this.source = new ArrayList<Object>(source); } public ICommand createCommand() { return DragAndDropCommand.create(domain, target, location, operations, operation, source); } } }