/** * Copyright (C) 2001-2017 by RapidMiner and the contributors * * Complete list of developers available at our web site: * * http://rapidminer.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.flow.processrendering.view; import java.awt.Point; import java.awt.Rectangle; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; import java.io.IOException; import java.util.Collections; import java.util.List; import java.util.logging.Level; import javax.swing.TransferHandler; import com.rapidminer.Process; import com.rapidminer.gui.RapidMinerGUI; import com.rapidminer.gui.dnd.ReceivingOperatorTransferHandler; import com.rapidminer.gui.flow.AutoWireThread; import com.rapidminer.gui.flow.processrendering.annotations.model.AnnotationsModel; import com.rapidminer.gui.flow.processrendering.annotations.model.ProcessAnnotation; import com.rapidminer.gui.flow.processrendering.annotations.model.WorkflowAnnotation; import com.rapidminer.gui.flow.processrendering.draw.ProcessDrawUtils; import com.rapidminer.gui.flow.processrendering.draw.ProcessDrawer; import com.rapidminer.gui.flow.processrendering.model.ProcessRendererModel; import com.rapidminer.gui.processeditor.XMLEditor; import com.rapidminer.operator.ExecutionUnit; import com.rapidminer.operator.Operator; import com.rapidminer.operator.OperatorChain; import com.rapidminer.operator.ProcessRootOperator; import com.rapidminer.operator.io.RepositorySource; import com.rapidminer.tools.I18N; import com.rapidminer.tools.LogService; import com.rapidminer.tools.XMLException; /** * The {@link TransferHandler} for the {@link ProcessRendererView}. * * @author Simon Fischer, Michael Knopf * @since 6.4.0 * */ public class ProcessRendererTransferHandler extends ReceivingOperatorTransferHandler { private static final long serialVersionUID = 1L; /** the view instance */ private final ProcessRendererView view; /** the model instance */ private final ProcessRendererModel model; /** the controller instance */ private final ProcessRendererController controller; /** the operator after which the dropped operator will be added */ private Operator dropInsertionPredecessor; public ProcessRendererTransferHandler(ProcessRendererView view, ProcessRendererModel model, ProcessRendererController controller) { this.view = view; this.model = model; this.controller = controller; } @Override public boolean dropNow(final List<Operator> newOperators, Point loc) { model.setImportDragged(false); model.fireMiscChanged(); controller.clearStatus(); if (newOperators.isEmpty()) { return true; } view.requestFocusInWindow(); // if we don't have a loc, we can use the mouse cursor if (loc == null) { if (model.getCurrentMousePosition() != null) { loc = new Point(model.getCurrentMousePosition()); } else { Rectangle viewing = RapidMinerGUI.getMainFrame().getProcessPanel().getViewPort().getViewRect(); loc = new Point((int) viewing.getCenterX(), (int) viewing.getCenterY()); } } // determine process to drop to int processIndex = view.getProcessIndexUnder(loc.getLocation()); try { if (processIndex != -1) { // we have a location for the drop/paste Operator firstOperator = newOperators.get(0); Point dest = view.toProcessSpace(loc, processIndex); // if we drop a single Retrieve operator on an inner source of the root // op, we immediately attach the repository location to the port. boolean isRoot = model.getDisplayedChain() instanceof ProcessRootOperator; boolean dropsSource = firstOperator instanceof RepositorySource; if (isRoot && dropsSource && newOperators.size() == 1) { if (controller.checkPortUnder(model.getProcess(processIndex).getInnerSources(), (int) dest.getX(), (int) dest.getY())) { String location = firstOperator.getParameters() .getParameterOrNull(RepositorySource.PARAMETER_REPOSITORY_ENTRY); int index = model.getHoveringPort().getPorts().getAllPorts().indexOf(model.getHoveringPort()); model.getDisplayedChain().getProcess().getContext().setInputRepositoryLocation(index, location); return true; } } // calculate operator position Point anchor; // snap to grid if (model.isSnapToGrid()) { anchor = ProcessDrawUtils.snap(new Point2D.Double(dest.getX() - ProcessDrawer.OPERATOR_WIDTH / 2, dest.getY() - ProcessDrawer.OPERATOR_MIN_HEIGHT / 2)); } else { anchor = new Point((int) dest.getX() - ProcessDrawer.OPERATOR_WIDTH / 2, (int) dest.getY() - ProcessDrawer.OPERATOR_MIN_HEIGHT / 2); } int index = view.getProcessIndexUnder(dest); if (index == -1) { index = 0; } // check if operator overlaps bottom process corner double lowestPosition = model.getProcessHeight(model.getProcess(index)) - ProcessDrawer.GRID_AUTOARRANGE_HEIGHT * model.getZoomFactor(); if (anchor.getY() > lowestPosition * 1 / model.getZoomFactor()) { anchor.setLocation(anchor.getX(), lowestPosition * 1 / model.getZoomFactor()); } // check if operator overlaps right process corner double rightestPositon = model.getProcessWidth(model.getProcess(index)) - ProcessDrawer.GRID_AUTOARRANGE_WIDTH * model.getZoomFactor(); if (anchor.getX() > rightestPositon * 1 / model.getZoomFactor()) { anchor.setLocation(rightestPositon * 1 / model.getZoomFactor(), anchor.getY()); } // check whether all operators have position data and compute upper // left anchor of the bounding box boolean completePositionData = true; double boundingBoxX = Double.MAX_VALUE; double boundingBoxY = Double.MAX_VALUE; double boundingBoxMaxX = 0; double boundingBoxMaxY = 0; for (Operator op : newOperators) { // check position data Rectangle2D rect = model.getOperatorRect(op); if (rect == null) { completePositionData = false; break; } // compare positions if (rect.getX() < boundingBoxX) { boundingBoxX = rect.getX(); } if (rect.getY() < boundingBoxY) { boundingBoxY = rect.getY(); } if (rect.getMaxX() > boundingBoxMaxX) { boundingBoxMaxX = rect.getMaxX(); } if (rect.getMaxY() > boundingBoxMaxY) { boundingBoxMaxY = rect.getMaxY(); } } if (completePositionData) { // adjust position relative to the computed anchor int dx = (int) (anchor.getX() - boundingBoxX); int dy = (int) (anchor.getY() - boundingBoxY); for (Operator op : newOperators) { Rectangle2D rect = model.getOperatorRect(op); Rectangle2D newRect = new java.awt.geom.Rectangle2D.Double(rect.getX() + dx, rect.getY() + dy, rect.getWidth(), rect.getHeight()); model.setOperatorRect(op, newRect); model.fireOperatorMoved(op); } // update process size (if necessary) controller.ensureWidth(model.getProcess(processIndex), (int) boundingBoxMaxX + dx); controller.ensureHeight(model.getProcess(processIndex), (int) boundingBoxMaxY + dy); } else { // position first operator at the anchor and remove position data of // remaining operator (if any) to trigger auto positioning Rectangle2D anchorRect = new Rectangle2D.Double(anchor.getX(), anchor.getY(), ProcessDrawer.OPERATOR_WIDTH, ProcessDrawer.OPERATOR_MIN_HEIGHT); int opIndex = 1; for (Operator op : newOperators) { if (op == firstOperator) { model.setOperatorRect(op, anchorRect); } else { Rectangle2D newAnchor = new Rectangle2D.Double(anchorRect.getMinX(), anchorRect.getMinY() + opIndex * (ProcessDrawer.GRID_Y_OFFSET + ProcessDrawer.OPERATOR_MIN_HEIGHT), anchorRect.getWidth(), anchorRect.getHeight()); model.setOperatorRect(op, newAnchor); opIndex++; } model.fireOperatorMoved(op); } } // index at which the first operator is inserted int firstInsertionIndex; final boolean firstMustBeWired; // insert first operator. Possibly insert into connection if (model.getHoveringConnectionSource() != null && ProcessDrawUtils.canOperatorBeInsertedIntoConnection(model, firstOperator)) { int predecessorIndex = model.getProcess(processIndex).getOperators() .indexOf(model.getHoveringConnectionSource().getPorts().getOwner().getOperator()); if (predecessorIndex != -1) { firstInsertionIndex = predecessorIndex + 1; } else { // can happen if dropIntersectsOutputPort is an inner source firstInsertionIndex = getDropInsertionIndex(processIndex); } model.getProcess(processIndex).addOperator(firstOperator, firstInsertionIndex); controller.insertIntoHoveringConnection(firstOperator); firstMustBeWired = false; } else { firstInsertionIndex = getDropInsertionIndex(processIndex); model.getProcess(processIndex).addOperator(firstOperator, firstInsertionIndex); firstMustBeWired = true; } // insert the rest (1..n). First, insert, then wire for (int i = 1; i < newOperators.size(); i++) { Operator newOp = newOperators.get(i); model.getProcess(processIndex).addOperator(newOp, firstInsertionIndex + i); } AutoWireThread.autoWireInBackground(newOperators, firstMustBeWired); boolean first = true; for (Operator op : newOperators) { controller.selectOperator(op, first); first = false; } dropInsertionPredecessor = null; return true; } else { dropInsertionPredecessor = null; return false; } } catch (RuntimeException e) { LogService.getRoot().log(Level.WARNING, I18N.getMessage(LogService.getRoot().getResourceBundle(), "com.rapidminer.gui.flow.ProcessRenderer.error_during_drop", e), e); throw e; } } @Override protected boolean dropNow(WorkflowAnnotation anno, Point loc) { if (anno == null) { return false; } AnnotationsModel annoModel = RapidMinerGUI.getMainFrame().getProcessPanel().getAnnotationsHandler().getModel(); // pasting an operator anno will always create a process anno if (loc == null) { loc = model.getMousePositionRelativeToProcess(); } int index = model.getHoveringProcessIndex(); ExecutionUnit targetProcess; if (index != -1) { targetProcess = model.getProcess(index); } else { targetProcess = anno.getProcess(); } ProcessAnnotation proAnno = anno.createProcessAnnotation(targetProcess); // move to drop location Rectangle2D frame = proAnno.getLocation(); Rectangle2D newFrame = new Rectangle2D.Double(loc.getX(), loc.getY(), frame.getWidth(), frame.getHeight()); proAnno.setLocation(newFrame); annoModel.addProcessAnnotation(proAnno); return true; } @Override protected boolean isDropLocationOk(final List<Operator> newOperators, final Point loc) { if (!view.isEnabled()) { return false; } if (view.getProcessIndexUnder(loc) == -1) { return false; } else { for (Operator newOperator : newOperators) { if (newOperator instanceof OperatorChain) { if (model.getDisplayedChain() == newOperator || ((OperatorChain) newOperator).getAllInnerOperators().contains(model.getDisplayedChain())) { return false; } } } return true; } } @Override protected void markDropOver(final Point dropPoint) { int pid = view.getProcessIndexUnder(dropPoint); if (pid != -1) { Point processSpace = view.toProcessSpace(dropPoint, pid); model.setHoveringConnectionSource(controller.getPortForConnectorNear(processSpace, model.getProcess(pid))); } model.fireMiscChanged(); } @Override protected List<Operator> getDraggedOperators() { // if a workflow annotation is selected, it takes precedence (only selected if no actual // operator is selected, except for the displayed chain) AnnotationsModel annoModel = RapidMinerGUI.getMainFrame().getProcessPanel().getAnnotationsHandler().getModel(); if (annoModel.getSelected() != null) { return Collections.<Operator> emptyList(); } return model.getSelectedOperators(); } @Override protected WorkflowAnnotation getDraggedAnnotation() { AnnotationsModel annoModel = RapidMinerGUI.getMainFrame().getProcessPanel().getAnnotationsHandler().getModel(); return annoModel.getSelected(); } @Override public boolean canImport(final TransferSupport ts) { if (ts.isDrop()) { int pid = view.getProcessIndexUnder(ts.getDropLocation().getDropPoint()); if (pid < 0) { return false; } } boolean canImport = controller.canImportTransferable(ts.getTransferable()); canImport &= super.canImport(ts); if (ts.isDrop() && model.isDropTargetSet() && !model.isDragStarted() && canImport && !model.isImportDragged()) { model.setImportDragged(true); model.fireMiscChanged(); } return canImport; } @Override protected void dropEnds() { // this prevents wrong drag target message which can occur if the mouseExited event on the // operator tree is not triggered when using Java 7 model.setOperatorSourceHovered(false); model.setImportDragged(false); model.fireMiscChanged(); } @Override protected Process getProcess() { return model.getDisplayedChain().getProcess(); } @Override protected boolean dropNow(String processXML) { ((XMLEditor) RapidMinerGUI.getMainFrame().getXMLEditor()).setText(processXML); try { ((XMLEditor) RapidMinerGUI.getMainFrame().getXMLEditor()).validateProcess(); } catch (IOException | XMLException e) { LogService.getRoot().log(Level.WARNING, "com.rapidminer.gui.processeditor.XMLEditor.failed_to_parse_process"); return false; } return true; } /** * Returns the index at which an operator should be inserted. The operator is inserted after * {@link #dropInsertionPredecessor} or as the last operator if * {@link #dropInsertionPredecessor} is null. */ private int getDropInsertionIndex(final int processIndex) { if (dropInsertionPredecessor == null) { return model.getProcess(processIndex).getOperators().size(); } else { return dropInsertionPredecessor.getExecutionUnit().getOperators().indexOf(dropInsertionPredecessor) + 1; } } }