/*
* RapidMiner
*
* Copyright (C) 2001-2008 by Rapid-I and the contributors
*
* Complete list of developers available at our web site:
*
* http://rapid-i.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see http://www.gnu.org/licenses/.
*/
package com.rapidminer.gui.operatortree;
import java.awt.Insets;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.Transferable;
import java.awt.dnd.DnDConstants;
import java.awt.dnd.DragGestureEvent;
import java.awt.dnd.DragGestureListener;
import java.awt.dnd.DragSourceDragEvent;
import java.awt.dnd.DragSourceDropEvent;
import java.awt.dnd.DragSourceEvent;
import java.awt.dnd.DragSourceListener;
import java.awt.dnd.DropTargetDragEvent;
import java.awt.dnd.DropTargetDropEvent;
import java.awt.dnd.DropTargetEvent;
import java.awt.dnd.DropTargetListener;
import java.awt.dnd.InvalidDnDOperationException;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.Arrays;
import java.util.List;
import javax.swing.Timer;
import javax.swing.tree.TreePath;
import com.rapidminer.gui.RapidMinerGUI;
import com.rapidminer.operator.Operator;
import com.rapidminer.operator.OperatorChain;
/**
* Provides all necessary implementations of standard DRAG & DROP features
* for the main process view of the RapidMiner GUI.
*
* @see com.rapidminer.gui.operatortree.OperatorTree
* @author Helge Homburg
* @version $Id: DnDSupport.java,v 1.5 2008/05/09 19:23:26 ingomierswa Exp $
*/
public class DnDSupport implements DropTargetListener, DragSourceListener, DragGestureListener {
/** The OperatorTree associated to this class */
private OperatorTree operatorTree;
/** Several fields for storage of mouse positions */
private Point previousCursorLocation = new Point();
private Point currentCursorLocation;
private Point operatorPosition;
/** Fields used for placing droplines at the right position */
private static String[] markedOperator = new String[3];
private int previousDnDMarker = 0;
private int currentDnDMarker = 0;
private boolean changeHappened;
public static final int fullMarker = 0;
public static final int upperMarker = 1;
public static final int lowerMarker = 2;
/** Several variables needed for temporary storage of path locations */
private TreePath originPath;
private TreePath currentPath;
private TreePath previousPath;
/** A timer needed for automatic node expansion */
private Timer nodeExpandDelay;
/** Defines all actions that are allowed for drag & drop */
private final Integer[] myDnDActions = {
Integer.valueOf(DnDConstants.ACTION_COPY),
Integer.valueOf(DnDConstants.ACTION_MOVE),
Integer.valueOf(DnDConstants.ACTION_COPY_OR_MOVE)
};
/** A list of all acceptable DnD-actions */
private final List acceptedDnDActions = Arrays.asList(myDnDActions);
// ------ Constructor ------------------------------------------------------------------
public DnDSupport(OperatorTree tree) {
this.operatorTree = tree;
setupNodeExpandTimer();
}
public static void setMarkedOperator(String[] marked) {
markedOperator = marked;
}
//----- methods from DropTargetListener -------------------------------------------------
public void dragEnter(DropTargetDragEvent e) {
if (dragAllowed(e) == false) {
e.rejectDrag();
return;
}
previousCursorLocation = e.getLocation();
e.acceptDrag(e.getDropAction());
}
public void drop(DropTargetDropEvent e) {
// test if DataFlavor and origin of the dragged object is acceptable
DataFlavor[] currentFlavors = TransferableOperator.DATA_FLAVORS;
DataFlavor acceptedFlavor = null;
if (e.isLocalTransfer() == false) {
acceptedFlavor = TransferableOperator.TRANSFERRED_OPERATOR_FLAVOR;
} else {
for (int i = 0; i < currentFlavors.length; i++) {
if (e.isDataFlavorSupported(currentFlavors[i])) {
acceptedFlavor = currentFlavors[i];
break;
}
;
}
}
if (acceptedFlavor == null) {
e.rejectDrop();
return;
}
// test if the drag action is supported
if (acceptedDnDActions.contains(Integer.valueOf(e.getDropAction())) == false) {
e.rejectDrop();
return;
}
// receive an operator from the transferable object
Transferable receivedOperator = null;
Operator newOperator = null;
try {
e.acceptDrop(e.getDropAction());
receivedOperator = e.getTransferable();
if (receivedOperator == null)
throw new NullPointerException();
if (receivedOperator.getTransferData(acceptedFlavor) instanceof Operator) {
newOperator = (Operator) receivedOperator.getTransferData(acceptedFlavor);
} else {
newOperator = null;
}
} catch (Throwable t) {
t.printStackTrace();
e.dropComplete(false);
return;
}
// determine a location for the drop action
Operator targetOperator, newParentOperator;
Point dropLocation = e.getLocation();
TreePath dropLocationPath = operatorTree.getClosestPathForLocation(dropLocation.x, dropLocation.y);
if (dropLocationPath != null) {
targetOperator = (Operator) dropLocationPath.getLastPathComponent();
} else {
e.dropComplete(false);
return;
}
if (targetOperator == null) {
e.dropComplete(false);
return;
}
// test if operator(chain) is illegally moved or copied into itself
boolean notDownDrag = true;
if (targetOperator.equals(newOperator))
notDownDrag = false;
OperatorChain moveUP = targetOperator.getParent();
while (true) {
if (moveUP == null)
break;
if (moveUP.equals(newOperator))
notDownDrag = false;
moveUP = moveUP.getParent();
}
if (notDownDrag) {
if (e.getDropAction() == DnDConstants.ACTION_MOVE) {
int indexDelete = newOperator.getParent().getIndexOfOperator(newOperator, true);
TreePath oldLocation = originPath.getParentPath();
newOperator.remove();
((OperatorTreeModel) operatorTree.getModel()).fireOperatorRemoved(this, oldLocation, indexDelete, newOperator);
} else if (e.getDropAction() == DnDConstants.ACTION_COPY) {
newOperator = newOperator.cloneOperator(newOperator.getName());
}
int indexOfTargetOperator = 0;
// finally insert the operator at the intended position
newParentOperator = null;
if (targetOperator.getParent() != null) {
indexOfTargetOperator = targetOperator.getParent().getIndexOfOperator(targetOperator, true);
newParentOperator = targetOperator.getParent();
}
int droplineIndicator = this.getOperatorMarker(targetOperator.getName());
int indexInsert = 0;
if (droplineIndicator < 0) {
e.dropComplete(false);
return;
}
// actually adding of operator into parent
if (droplineIndicator == 0 || droplineIndicator > 2) {
indexInsert = ((OperatorChain) targetOperator).addOperator(newOperator);
}
if (droplineIndicator == 1) {
if (indexOfTargetOperator >= 0)
indexInsert = indexOfTargetOperator;
((OperatorChain) newParentOperator).addOperator(newOperator, indexInsert);
dropLocationPath = dropLocationPath.getParentPath();
}
if (droplineIndicator == 2) {
if (indexOfTargetOperator < ((OperatorChain) newParentOperator).getNumberOfAllOperators())
indexInsert = indexOfTargetOperator + 1;
((OperatorChain) newParentOperator).addOperator(newOperator, indexInsert);
dropLocationPath = dropLocationPath.getParentPath();
}
// end of adding
((OperatorTreeModel) operatorTree.getModel()).fireOperatorInserted(this, dropLocationPath, indexInsert, newOperator);
operatorTree.scrollPathToVisible(dropLocationPath.pathByAddingChild(newOperator));
RapidMinerGUI.getMainFrame().processChanged();
} else {
e.dropComplete(true);
return;
}
clearOperatorMarker();
e.dropComplete(true);
}
public void dragOver(DropTargetDragEvent e) {
if (dragAllowed(e) == false) {
e.rejectDrag();
return;
}
currentCursorLocation = e.getLocation();
if ((!currentCursorLocation.equals(previousCursorLocation))) {
currentPath = operatorTree.getClosestPathForLocation(currentCursorLocation.x, currentCursorLocation.y);
Rectangle dropActionTriggerArea = operatorTree.getPathBounds(currentPath);
if (dropActionTriggerArea != null) {
Operator currentDropZone = (Operator) currentPath.getLastPathComponent();
if (currentDropZone != null) {
// start the computation of the dropline position
operatorPosition = operatorTree.getPathBounds(currentPath).getLocation();
double operatorHeight = operatorTree.getPathBounds(currentPath).getHeight();
long aQuarter = Math.round(operatorHeight / 4);
long aHalf = Math.round(operatorHeight / 2);
long currentPartition;
boolean dropTargetIsOperatorChain = currentDropZone instanceof OperatorChain;
if (dropTargetIsOperatorChain) {
currentPartition = aQuarter;
} else {currentPartition = aHalf; }
String[] cleanMarkedOperator = { "", "", "" };
setMarkedOperator(cleanMarkedOperator);
if (currentDropZone.getParent() != null) {
if (currentCursorLocation.y < (operatorPosition.y + currentPartition)) {
markedOperator[1] = currentDropZone.getName();
currentDnDMarker = 1;
}
if (currentCursorLocation.y > (operatorPosition.y + operatorHeight - currentPartition)) {
markedOperator[2] = currentDropZone.getName();
currentDnDMarker = 2;
}
}
if (dropTargetIsOperatorChain) {
if (((currentCursorLocation.y >= (operatorPosition.y + aQuarter)) && (currentCursorLocation.y <= (operatorPosition.y + operatorHeight - aQuarter))) || currentDropZone.getParent() == null) {
markedOperator[0] = currentDropZone.getName();
currentDnDMarker = 0;
// start the timer for automatic nodeexpansion
nodeExpandDelay.restart();
}
}
if ((previousDnDMarker != currentDnDMarker) || (previousPath != currentPath))
changeHappened = true;
if (changeHappened)
operatorTree.treeDidChange();
changeHappened = false;
previousDnDMarker = currentDnDMarker;
previousPath = currentPath;
operatorTree.treeDidChange();
}
}
// autoscroll to possible dropzone
Insets insets = new Insets(40, 40, 40, 40);
Rectangle currentlyVisible = operatorTree.getVisibleRect();
Rectangle validCursorArea = new Rectangle(currentlyVisible.x + insets.left, currentlyVisible.y + insets.top, currentlyVisible.width - (insets.left + insets.right), currentlyVisible.height - (insets.top + insets.bottom));
if (!validCursorArea.contains(currentCursorLocation)) {
Rectangle updatedArea = new Rectangle(currentCursorLocation.x - insets.left, currentCursorLocation.y - insets.top, insets.left + insets.right, insets.top + insets.bottom);
operatorTree.scrollRectToVisible(updatedArea);
}
previousCursorLocation = currentCursorLocation;
}
e.acceptDrag(e.getDropAction());
}
public void dropActionChanged(DropTargetDragEvent e) {
if (dragAllowed(e) == false) {
e.rejectDrag();
return;
}
e.acceptDrag(e.getDropAction());
}
public void dragExit(DropTargetEvent e) {
clearOperatorMarker();
}
// ------ methods from DragSourceListener ------------------------------------------------
public void dragDropEnd(DragSourceDropEvent e) {
clearOperatorMarker();
operatorTree.setEditable(true);
if (!e.getDropSuccess()) {
DataFlavor acceptedFlavor = TransferableOperator.TRANSFERRED_OPERATOR_FLAVOR;
try {
Transferable currentlyDraggedOperator = e.getDragSourceContext().getTransferable();
if (currentlyDraggedOperator == null)
throw new NullPointerException();
if (currentlyDraggedOperator.getTransferData(acceptedFlavor) instanceof Operator) {
Operator newOperator = (Operator) currentlyDraggedOperator.getTransferData(acceptedFlavor);
int indexDelete = newOperator.getParent().getIndexOfOperator(newOperator, true);
TreePath oldLocation = originPath.getParentPath();
newOperator.remove();
((OperatorTreeModel) operatorTree.getModel()).fireOperatorRemoved(this, oldLocation, indexDelete, newOperator);
RapidMinerGUI.getMainFrame().processChanged();
}
} catch (Throwable t) {
t.printStackTrace();
return;
}
}
}
public void dragEnter(DragSourceDragEvent e) {
}
public void dragExit(DragSourceEvent e) {
}
public void dragOver(DragSourceDragEvent e) {
}
public void dropActionChanged(DragSourceDragEvent e) {
}
//------ methods from DragGestureListener -----------------------------------------------
public void dragGestureRecognized(DragGestureEvent e) {
// determine the operator at the current postion of the mouse pointer and make it transferable
if (acceptedDnDActions.contains(Integer.valueOf(e.getDragAction())) == false)
return;
if (operatorTree.isStructureLocked())
return;
Point dragOrigin = e.getDragOrigin();
originPath = operatorTree.getPathForLocation(dragOrigin.x, dragOrigin.y);
// make clear that the originPath variable points to an existing entity AND is different to the root node
if (originPath != null && originPath.getParentPath() != null) {
Operator selectedOperator = (Operator) originPath.getLastPathComponent();
if (selectedOperator != null) {
TransferableOperator selectedTransferableOperator = new TransferableOperator(selectedOperator);
try {
operatorTree.setEditable(false);
e.startDrag(null, null, new Point(0, 0), selectedTransferableOperator, this);
} catch (InvalidDnDOperationException dndE) {
dndE.printStackTrace();
}
}
}
}
//------ methods from DnDSupport ----------------------------------------------------
private boolean dragAllowed(DropTargetDragEvent e) {
if (operatorTree.isStructureLocked())
return false;
DataFlavor[] currentFlavors = TransferableOperator.DATA_FLAVORS;
DataFlavor acceptedFlavor = null;
for (int i = 0; i < currentFlavors.length; i++) {
if (e.isDataFlavorSupported(currentFlavors[i])) {
acceptedFlavor = currentFlavors[i];
break;
}
;
}
if (acceptedFlavor == null)
return false;
if (acceptedDnDActions.contains(Integer.valueOf(e.getSourceActions())) == false)
return false;
return true;
}
public int getOperatorMarker(String operatorName) {
List markerList = Arrays.asList(markedOperator);
return markerList.indexOf(operatorName);
}
private void clearOperatorMarker(){
nodeExpandDelay.stop();
String[] cleanMarkedOperator = { "", "", "" };
markedOperator = cleanMarkedOperator;
operatorTree.treeDidChange();
}
private void setupNodeExpandTimer() {
nodeExpandDelay = new Timer(1500, new ActionListener() {
public void actionPerformed(ActionEvent e) {
if (operatorTree.isRootVisible() && operatorTree.getRowForPath(currentPath) == 0) {
return;
} else {
if (operatorTree.isExpanded(currentPath)) {
operatorTree.collapsePath(currentPath);
} else {
operatorTree.expandPath(currentPath);
}
}
}
});
nodeExpandDelay.setRepeats(false);
}
}