/**
* 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.Component;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.datatransfer.Transferable;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.event.FocusAdapter;
import java.awt.event.FocusEvent;
import java.awt.event.HierarchyEvent;
import java.awt.event.HierarchyListener;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.logging.Level;
import javax.swing.Action;
import javax.swing.JComponent;
import javax.swing.JMenu;
import javax.swing.JMenuItem;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.JTextField;
import javax.swing.SwingConstants;
import javax.swing.SwingUtilities;
import com.rapidminer.BreakpointListener;
import com.rapidminer.Process;
import com.rapidminer.ProcessLocation;
import com.rapidminer.core.license.ProductConstraintManager;
import com.rapidminer.gui.MainFrame;
import com.rapidminer.gui.RapidMinerGUI;
import com.rapidminer.gui.actions.ConnectPortToRepositoryAction;
import com.rapidminer.gui.actions.StoreInRepositoryAction;
import com.rapidminer.gui.actions.export.PrintableComponent;
import com.rapidminer.gui.animation.OperatorAnimationProcessListener;
import com.rapidminer.gui.dnd.AbstractPatchedTransferHandler;
import com.rapidminer.gui.dnd.DragListener;
import com.rapidminer.gui.dnd.OperatorTransferHandler;
import com.rapidminer.gui.flow.ExtensionButton;
import com.rapidminer.gui.flow.OverviewPanel;
import com.rapidminer.gui.flow.PanningManager;
import com.rapidminer.gui.flow.ProcessInteractionListener;
import com.rapidminer.gui.flow.ProcessPanel;
import com.rapidminer.gui.flow.processrendering.annotations.model.WorkflowAnnotation;
import com.rapidminer.gui.flow.processrendering.annotations.model.WorkflowAnnotations;
import com.rapidminer.gui.flow.processrendering.draw.OperatorDrawDecorator;
import com.rapidminer.gui.flow.processrendering.draw.ProcessDrawDecorator;
import com.rapidminer.gui.flow.processrendering.draw.ProcessDrawUtils;
import com.rapidminer.gui.flow.processrendering.draw.ProcessDrawer;
import com.rapidminer.gui.flow.processrendering.event.ProcessRendererAnnotationEvent;
import com.rapidminer.gui.flow.processrendering.event.ProcessRendererEventListener;
import com.rapidminer.gui.flow.processrendering.event.ProcessRendererModelEvent;
import com.rapidminer.gui.flow.processrendering.event.ProcessRendererOperatorEvent;
import com.rapidminer.gui.flow.processrendering.model.ProcessRendererModel;
import com.rapidminer.gui.flow.processrendering.view.ProcessEventDecorator.KeyEventType;
import com.rapidminer.gui.flow.processrendering.view.ProcessEventDecorator.MouseEventType;
import com.rapidminer.gui.flow.processrendering.view.actions.ArrangeOperatorsAction;
import com.rapidminer.gui.flow.processrendering.view.actions.AutoFitAction;
import com.rapidminer.gui.flow.processrendering.view.actions.DeleteSelectedConnectionAction;
import com.rapidminer.gui.flow.processrendering.view.actions.RenameAction;
import com.rapidminer.gui.flow.processrendering.view.actions.SelectAllAction;
import com.rapidminer.gui.flow.processrendering.view.components.ProcessRendererTooltipProvider;
import com.rapidminer.gui.tools.PrintingTools;
import com.rapidminer.gui.tools.ResourceAction;
import com.rapidminer.gui.tools.ResourceMenu;
import com.rapidminer.gui.tools.SwingTools;
import com.rapidminer.gui.tools.components.ToolTipWindow;
import com.rapidminer.gui.tools.components.ToolTipWindow.TooltipLocation;
import com.rapidminer.license.LicenseEvent;
import com.rapidminer.license.LicenseEvent.LicenseEventType;
import com.rapidminer.license.LicenseManagerListener;
import com.rapidminer.operator.ExecutionUnit;
import com.rapidminer.operator.IOObject;
import com.rapidminer.operator.Operator;
import com.rapidminer.operator.OperatorChain;
import com.rapidminer.operator.ProcessRootOperator;
import com.rapidminer.operator.ResultObject;
import com.rapidminer.operator.ports.InputPort;
import com.rapidminer.operator.ports.OutputPort;
import com.rapidminer.operator.ports.Port;
import com.rapidminer.operator.ports.quickfix.QuickFix;
import com.rapidminer.repository.RepositoryLocation;
import com.rapidminer.tools.I18N;
import com.rapidminer.tools.LogService;
import com.rapidminer.tools.SystemInfoUtilities;
import com.rapidminer.tools.SystemInfoUtilities.OperatingSystem;
/**
* This class displays a RapidMiner process and allows user interaction with it.
* <p>
* Actual Java2D drawing is delegated to the {@link ProcessDrawer} and its registered
* {@link ProcessDrawDecorator}s. To decorate the process drawing, call
* {@link #addDrawDecorator(ProcessDrawDecorator, RenderPhase)} and register custom decorators.
* </p>
* <p>
* To provide hooks into the event handling, i.e. to allow the user to interact with your
* decorations, event decorators can be registered via
* {@link #addEventDecorator(ProcessEventDecorator, RenderPhase)}.
* </p>
* <p>
* To simply hook into default popup menus, register a listener via
* {@link #addProcessInteractionListener(ProcessInteractionListener)}.
* </p>
*
* @author Simon Fischer, Marco Boeck, Jan Czogalla
* @since 6.4.0
*
*/
public class ProcessRendererView extends JPanel implements PrintableComponent {
private static final long serialVersionUID = 1L;
/** the text field used for renaming an operator */
private JTextField renameField;
/** responsible for default process renderer view interaction */
private final transient ProcessRendererMouseHandler interactionMouseHandler;
/** the mouse handler for the entire process renderer */
private final transient MouseAdapter mouseHandler = new MouseAdapter() {
@Override
public void mouseMoved(final MouseEvent e) {
// no matter what, update mouse locations first
model.setCurrentMousePosition(e.getPoint());
model.setHoveringProcessIndex(getProcessIndexUnder(e.getPoint()));
int hoveringProcessIndex = model.getHoveringProcessIndex();
if (model.getHoveringProcessIndex() != -1) {
model.setMousePositionRelativeToProcess(toProcessSpace(e.getPoint(), hoveringProcessIndex));
}
// foreground, overlay and operator addition listeners can be notified before operator
// hovering
boolean wasConsumed = false;
wasConsumed |= processPhaseListenerMouseEvent(MouseEventType.MOUSE_MOVED, e, RenderPhase.FOREGROUND);
if (wasConsumed) {
return;
}
wasConsumed |= processPhaseListenerMouseEvent(MouseEventType.MOUSE_MOVED, e, RenderPhase.OVERLAY);
if (wasConsumed) {
return;
}
wasConsumed |= processPhaseListenerMouseEvent(MouseEventType.MOUSE_MOVED, e, RenderPhase.OPERATOR_ADDITIONS);
if (wasConsumed) {
return;
}
interactionMouseHandler.mouseMoved(e);
if (e.isConsumed()) {
return;
}
// if core handling did not consume, forward event further down
wasConsumed |= processPhaseListenerMouseEvent(MouseEventType.MOUSE_MOVED, e, RenderPhase.OPERATORS);
if (wasConsumed) {
return;
}
wasConsumed |= processPhaseListenerMouseEvent(MouseEventType.MOUSE_MOVED, e, RenderPhase.CONNECTIONS);
if (wasConsumed) {
return;
}
wasConsumed |= processPhaseListenerMouseEvent(MouseEventType.MOUSE_MOVED, e, RenderPhase.OPERATOR_BACKGROUND);
if (wasConsumed) {
return;
}
wasConsumed |= processPhaseListenerMouseEvent(MouseEventType.MOUSE_MOVED, e, RenderPhase.OPERATOR_ANNOTATIONS);
if (wasConsumed) {
return;
}
wasConsumed |= processPhaseListenerMouseEvent(MouseEventType.MOUSE_MOVED, e, RenderPhase.ANNOTATIONS);
if (wasConsumed) {
return;
}
wasConsumed |= processPhaseListenerMouseEvent(MouseEventType.MOUSE_MOVED, e, RenderPhase.BACKGROUND);
if (wasConsumed) {
return;
}
}
@Override
public void mouseDragged(final MouseEvent e) {
// no matter what, update mouse locations first
model.setCurrentMousePosition(e.getPoint());
model.setHoveringProcessIndex(getProcessIndexUnder(e.getPoint()));
int hoveringProcessIndex = model.getHoveringProcessIndex();
if (model.getHoveringProcessIndex() != -1) {
model.setMousePositionRelativeToProcess(toProcessSpace(e.getPoint(), hoveringProcessIndex));
}
// foreground, overlay and operator addition listeners can be notified before operator
// hovering
boolean wasConsumed = false;
wasConsumed |= processPhaseListenerMouseEvent(MouseEventType.MOUSE_DRAGGED, e, RenderPhase.FOREGROUND);
if (wasConsumed) {
return;
}
wasConsumed |= processPhaseListenerMouseEvent(MouseEventType.MOUSE_DRAGGED, e, RenderPhase.OVERLAY);
if (wasConsumed) {
return;
}
wasConsumed |= processPhaseListenerMouseEvent(MouseEventType.MOUSE_DRAGGED, e, RenderPhase.OPERATOR_ADDITIONS);
if (wasConsumed) {
return;
}
interactionMouseHandler.mouseDragged(e);
if (e.isConsumed()) {
return;
}
// if core handling did not consume, forward event further down
wasConsumed |= processPhaseListenerMouseEvent(MouseEventType.MOUSE_DRAGGED, e, RenderPhase.OPERATORS);
if (wasConsumed) {
return;
}
wasConsumed |= processPhaseListenerMouseEvent(MouseEventType.MOUSE_DRAGGED, e, RenderPhase.CONNECTIONS);
if (wasConsumed) {
return;
}
wasConsumed |= processPhaseListenerMouseEvent(MouseEventType.MOUSE_DRAGGED, e, RenderPhase.OPERATOR_BACKGROUND);
if (wasConsumed) {
return;
}
wasConsumed |= processPhaseListenerMouseEvent(MouseEventType.MOUSE_DRAGGED, e, RenderPhase.OPERATOR_ANNOTATIONS);
if (wasConsumed) {
return;
}
wasConsumed |= processPhaseListenerMouseEvent(MouseEventType.MOUSE_DRAGGED, e, RenderPhase.ANNOTATIONS);
if (wasConsumed) {
return;
}
wasConsumed |= processPhaseListenerMouseEvent(MouseEventType.MOUSE_DRAGGED, e, RenderPhase.BACKGROUND);
if (wasConsumed) {
return;
}
}
@Override
public void mousePressed(final MouseEvent e) {
// whatever we pressed the mouse on, remove the rename field
if (renameField != null) {
remove(renameField);
}
requestFocusInWindow();
// foreground, overlay and operator addition listeners can be notified before operator
// modifications
boolean wasConsumed = false;
wasConsumed |= processPhaseListenerMouseEvent(MouseEventType.MOUSE_PRESSED, e, RenderPhase.FOREGROUND);
if (wasConsumed) {
return;
}
wasConsumed |= processPhaseListenerMouseEvent(MouseEventType.MOUSE_PRESSED, e, RenderPhase.OVERLAY);
if (wasConsumed) {
return;
}
wasConsumed |= processPhaseListenerMouseEvent(MouseEventType.MOUSE_PRESSED, e, RenderPhase.OPERATOR_ADDITIONS);
if (wasConsumed) {
return;
}
interactionMouseHandler.mousePressed(e);
if (e.isConsumed()) {
return;
}
// if core handling did not consume, forward event further down
wasConsumed |= processPhaseListenerMouseEvent(MouseEventType.MOUSE_PRESSED, e, RenderPhase.OPERATORS);
if (wasConsumed) {
return;
}
wasConsumed |= processPhaseListenerMouseEvent(MouseEventType.MOUSE_PRESSED, e, RenderPhase.CONNECTIONS);
if (wasConsumed) {
return;
}
wasConsumed |= processPhaseListenerMouseEvent(MouseEventType.MOUSE_PRESSED, e, RenderPhase.OPERATOR_BACKGROUND);
if (wasConsumed) {
return;
}
wasConsumed |= processPhaseListenerMouseEvent(MouseEventType.MOUSE_PRESSED, e, RenderPhase.OPERATOR_ANNOTATIONS);
if (wasConsumed) {
return;
}
wasConsumed |= processPhaseListenerMouseEvent(MouseEventType.MOUSE_PRESSED, e, RenderPhase.ANNOTATIONS);
if (wasConsumed) {
return;
}
interactionMouseHandler.mousePressedBackground(e);
if (e.isConsumed()) {
return;
}
wasConsumed |= processPhaseListenerMouseEvent(MouseEventType.MOUSE_PRESSED, e, RenderPhase.BACKGROUND);
if (wasConsumed) {
return;
}
}
@Override
public void mouseReleased(final MouseEvent e) {
// foreground, overlay and operator addition listeners can be notified before operator
// modifications
boolean wasConsumed = false;
wasConsumed |= processPhaseListenerMouseEvent(MouseEventType.MOUSE_RELEASED, e, RenderPhase.FOREGROUND);
if (wasConsumed) {
return;
}
wasConsumed |= processPhaseListenerMouseEvent(MouseEventType.MOUSE_RELEASED, e, RenderPhase.OVERLAY);
if (wasConsumed) {
return;
}
wasConsumed |= processPhaseListenerMouseEvent(MouseEventType.MOUSE_RELEASED, e, RenderPhase.OPERATOR_ADDITIONS);
if (wasConsumed) {
return;
}
interactionMouseHandler.mouseReleased(e);
if (e.isConsumed()) {
return;
}
// if core handling did not consume, forward event further down
wasConsumed |= processPhaseListenerMouseEvent(MouseEventType.MOUSE_RELEASED, e, RenderPhase.OPERATORS);
if (wasConsumed) {
return;
}
wasConsumed |= processPhaseListenerMouseEvent(MouseEventType.MOUSE_RELEASED, e, RenderPhase.CONNECTIONS);
if (wasConsumed) {
return;
}
wasConsumed |= processPhaseListenerMouseEvent(MouseEventType.MOUSE_RELEASED, e, RenderPhase.OPERATOR_BACKGROUND);
if (wasConsumed) {
return;
}
wasConsumed |= processPhaseListenerMouseEvent(MouseEventType.MOUSE_RELEASED, e,
RenderPhase.OPERATOR_ANNOTATIONS);
if (wasConsumed) {
return;
}
wasConsumed |= processPhaseListenerMouseEvent(MouseEventType.MOUSE_RELEASED, e, RenderPhase.ANNOTATIONS);
if (wasConsumed) {
return;
}
interactionMouseHandler.mouseReleasedBackground(e);
if (e.isConsumed()) {
return;
}
wasConsumed |= processPhaseListenerMouseEvent(MouseEventType.MOUSE_RELEASED, e, RenderPhase.BACKGROUND);
if (wasConsumed) {
return;
}
}
@Override
public void mouseClicked(final MouseEvent e) {
// foreground, overlay and operator addition listeners can be notified before operator
// modifications
boolean wasConsumed = false;
wasConsumed |= processPhaseListenerMouseEvent(MouseEventType.MOUSE_CLICKED, e, RenderPhase.FOREGROUND);
if (wasConsumed) {
return;
}
wasConsumed |= processPhaseListenerMouseEvent(MouseEventType.MOUSE_CLICKED, e, RenderPhase.OVERLAY);
if (wasConsumed) {
return;
}
wasConsumed |= processPhaseListenerMouseEvent(MouseEventType.MOUSE_CLICKED, e, RenderPhase.OPERATOR_ADDITIONS);
if (wasConsumed) {
return;
}
interactionMouseHandler.mouseClicked(e);
if (e.isConsumed()) {
return;
}
// if core handling did not consume, forward event further down
wasConsumed |= processPhaseListenerMouseEvent(MouseEventType.MOUSE_CLICKED, e, RenderPhase.OPERATORS);
if (wasConsumed) {
return;
}
wasConsumed |= processPhaseListenerMouseEvent(MouseEventType.MOUSE_CLICKED, e, RenderPhase.CONNECTIONS);
if (wasConsumed) {
return;
}
wasConsumed |= processPhaseListenerMouseEvent(MouseEventType.MOUSE_CLICKED, e, RenderPhase.OPERATOR_BACKGROUND);
if (wasConsumed) {
return;
}
wasConsumed |= processPhaseListenerMouseEvent(MouseEventType.MOUSE_CLICKED, e, RenderPhase.OPERATOR_ANNOTATIONS);
if (wasConsumed) {
return;
}
wasConsumed |= processPhaseListenerMouseEvent(MouseEventType.MOUSE_CLICKED, e, RenderPhase.ANNOTATIONS);
if (wasConsumed) {
return;
}
wasConsumed |= processPhaseListenerMouseEvent(MouseEventType.MOUSE_CLICKED, e, RenderPhase.BACKGROUND);
if (wasConsumed) {
return;
}
}
@Override
public void mouseEntered(final MouseEvent e) {
interactionMouseHandler.mouseEntered(e);
// first come, first served, no limit to specific phases
boolean wasConsumed = false;
for (RenderPhase phase : RenderPhase.eventOrder()) {
wasConsumed |= processPhaseListenerMouseEvent(MouseEventType.MOUSE_ENTERED, e, phase);
// abort if event was consumed
if (wasConsumed) {
return;
}
}
}
@Override
public void mouseExited(final MouseEvent e) {
if (!SwingTools.isMouseEventExitedToChildComponents(ProcessRendererView.this, e)) {
model.setCurrentMousePosition(null);
}
// always reset status text
interactionMouseHandler.mouseExited(e);
// first come, first served, no limit to specific phases
boolean wasConsumed = false;
for (RenderPhase phase : RenderPhase.eventOrder()) {
wasConsumed |= processPhaseListenerMouseEvent(MouseEventType.MOUSE_EXITED, e, phase);
// abort if event was consumed
if (wasConsumed) {
return;
}
}
};
};
/** the key handler for the entire process renderer */
private final transient KeyAdapter keyHandler = new KeyAdapter() {
@Override
public void keyPressed(final KeyEvent e) {
boolean wasConsumed = false;
switch (e.getKeyCode()) {
case KeyEvent.VK_LEFT:
case KeyEvent.VK_RIGHT:
case KeyEvent.VK_UP:
case KeyEvent.VK_DOWN:
wasConsumed |= processPhaseListenerKeyEvent(KeyEventType.KEY_PRESSED, e, RenderPhase.FOREGROUND);
if (wasConsumed) {
return;
}
wasConsumed |= processPhaseListenerKeyEvent(KeyEventType.KEY_PRESSED, e, RenderPhase.OVERLAY);
if (wasConsumed) {
return;
}
wasConsumed |= processPhaseListenerKeyEvent(KeyEventType.KEY_PRESSED, e, RenderPhase.OPERATOR_ADDITIONS);
if (wasConsumed) {
return;
}
// operator phase event, no more decorator processing afterwards
controller.selectInDirection(e);
e.consume();
break;
case KeyEvent.VK_ESCAPE:
// remove currently dragged connection. Afterwards: first come, first served
boolean abortedConnection = false;
if (model.getConnectingPortSource() != null) {
model.setConnectingPortSource(null);
model.fireMiscChanged();
abortedConnection = true;
}
// process render phases that come before the OPERATORS phase, abort in case the
// event was consumed
for (RenderPhase phase : new RenderPhase[] { RenderPhase.FOREGROUND, RenderPhase.OVERLAY,
RenderPhase.OPERATOR_ADDITIONS }) {
wasConsumed |= processPhaseListenerKeyEvent(KeyEventType.KEY_PRESSED, e, phase);
if (wasConsumed) {
return;
}
}
// OPERATORS phase event in case we are in a nested operator (consume event and
// abort)
if (!abortedConnection && model.getDisplayedChain().getRoot() != model.getDisplayedChain()) {
OperatorChain parent = model.getDisplayedChain().getParent();
if (parent != null) {
model.setDisplayedChainAndFire(parent);
}
e.consume();
return;
}
// remaining render phases, abort in case the event was consumed
for (RenderPhase phase : new RenderPhase[] { RenderPhase.CONNECTIONS, RenderPhase.OPERATOR_BACKGROUND,
RenderPhase.OPERATOR_ANNOTATIONS, RenderPhase.ANNOTATIONS, RenderPhase.BACKGROUND }) {
wasConsumed |= processPhaseListenerKeyEvent(KeyEventType.KEY_PRESSED, e, phase);
if (wasConsumed) {
return;
}
}
break;
case KeyEvent.VK_ENTER:
wasConsumed |= processPhaseListenerKeyEvent(KeyEventType.KEY_PRESSED, e, RenderPhase.FOREGROUND);
if (wasConsumed) {
return;
}
wasConsumed |= processPhaseListenerKeyEvent(KeyEventType.KEY_PRESSED, e, RenderPhase.OVERLAY);
if (wasConsumed) {
return;
}
wasConsumed |= processPhaseListenerKeyEvent(KeyEventType.KEY_PRESSED, e, RenderPhase.OPERATOR_ADDITIONS);
if (wasConsumed) {
return;
}
// operator phase event, no more decorator processing afterwards
if (!model.getSelectedOperators().isEmpty()) {
Operator selected = model.getSelectedOperators().get(0);
if (selected instanceof OperatorChain) {
model.setDisplayedChainAndFire((OperatorChain) selected);
}
}
e.consume();
break;
case KeyEvent.VK_BACK_SPACE:
wasConsumed |= processPhaseListenerKeyEvent(KeyEventType.KEY_PRESSED, e, RenderPhase.FOREGROUND);
if (wasConsumed) {
return;
}
wasConsumed |= processPhaseListenerKeyEvent(KeyEventType.KEY_PRESSED, e, RenderPhase.OVERLAY);
if (wasConsumed) {
return;
}
wasConsumed |= processPhaseListenerKeyEvent(KeyEventType.KEY_PRESSED, e, RenderPhase.OPERATOR_ADDITIONS);
if (wasConsumed) {
return;
}
// OS X: Trigger deletion action.
// Other: Navigate up (same as ESC).
if (SystemInfoUtilities.getOperatingSystem() == OperatingSystem.OSX) {
deleteSelectedAction.actionPerformed(null);
} else if (model.getDisplayedChain().getRoot() != model.getDisplayedChain()) {
OperatorChain parent = model.getDisplayedChain().getParent();
if (parent != null) {
model.setDisplayedChainAndFire(parent);
}
}
// operator phase event, no more decorator processing afterwards
e.consume();
break;
default:
// first come, first served, no limit to specific phases
for (RenderPhase phase : RenderPhase.eventOrder()) {
wasConsumed |= processPhaseListenerKeyEvent(KeyEventType.KEY_PRESSED, e, phase);
// abort if event was consumed
if (wasConsumed) {
return;
}
}
}
}
@Override
public void keyReleased(KeyEvent e) {
boolean wasConsumed = false;
// first come, first served
for (RenderPhase phase : RenderPhase.eventOrder()) {
wasConsumed |= processPhaseListenerKeyEvent(KeyEventType.KEY_RELEASED, e, phase);
// abort if event was consumed
if (wasConsumed) {
return;
}
}
}
@Override
public void keyTyped(KeyEvent e) {
boolean wasConsumed = false;
// first come, first served
for (RenderPhase phase : RenderPhase.eventOrder()) {
wasConsumed |= processPhaseListenerKeyEvent(KeyEventType.KEY_TYPED, e, phase);
// abort if event was consumed
if (wasConsumed) {
return;
}
}
}
};
/** the drag and drop listener */
private final transient DragListener dragListener = new DragListener() {
@Override
public void dragStarted(Transferable t) {
// check if transferable can be imported
if (!controller.canImportTransferable(t)) {
return;
}
model.setDragStarted(true);
model.fireMiscChanged();
}
@Override
public void dragEnded() {
model.setDragStarted(false);
model.fireMiscChanged();
}
};
private final ResourceAction renameAction;
private final ResourceAction selectAllAction;
private final ResourceAction deleteSelectedAction;
private final Action arrangeOperatorsAction;
private final Action autoFitAction;
/** the list of extension buttons for subprocesses (add/remove subprocess) */
private final List<ExtensionButton> subprocessExtensionButtons;
/** list of interaction listeners */
private final List<ProcessInteractionListener> processInteractionListeners;
/** the list of event decorators */
private final Map<RenderPhase, CopyOnWriteArrayList<ProcessEventDecorator>> decorators;
/** the backing model */
private final ProcessRendererModel model;
/** the controller for this view */
private final ProcessRendererController controller;
/** the drawer responsible for 2D drawing the process for this view */
private final ProcessDrawer drawer;
/** the drawer responsible for 2D drawing the process for the overview */
private final ProcessDrawer drawerOverview;
/** the overview panel */
private final OverviewPanel overviewPanel;
/** the mainframe instance */
private final MainFrame mainFrame;
/** the repaint filter to ensure a minimal interval between repaints */
private final RepaintFilter repaintFilter;
public ProcessRendererView(final ProcessRendererModel model, final ProcessPanel processPanel,
final MainFrame mainFrame) {
this.mainFrame = mainFrame;
this.model = model;
this.controller = new ProcessRendererController(this, model);
this.drawer = new ProcessDrawer(model, true);
this.drawerOverview = new ProcessDrawer(model, false);
this.repaintFilter = new RepaintFilter(this);
// initialize process listener for animations
new OperatorAnimationProcessListener(model);
// prepare event decorators for each phase
decorators = new HashMap<>();
for (RenderPhase phase : RenderPhase.eventOrder()) {
decorators.put(phase, new CopyOnWriteArrayList<ProcessEventDecorator>());
}
overviewPanel = new OverviewPanel(this);
interactionMouseHandler = new ProcessRendererMouseHandler(this, model, controller);
// init list of subprocess extension buttons (add/remove subprocess)
subprocessExtensionButtons = new LinkedList<>();
// init listener list
processInteractionListeners = new LinkedList<>();
// listen to ProcessRendererModel events
model.registerEventListener(new ProcessRendererEventListener() {
@Override
public void modelChanged(ProcessRendererModelEvent e) {
switch (e.getEventType()) {
case DISPLAYED_CHAIN_CHANGED:
controller.processDisplayedChainChanged();
// notify registered listeners
fireDisplayedChainChanged(model.getDisplayedChain());
break;
case DISPLAYED_PROCESSES_CHANGED:
controller.setInitialSizes();
setupExtensionButtons();
break;
case PROCESS_ZOOM_CHANGED:
controller.autoFit();
//$FALL-THROUGH$
case PROCESS_SIZE_CHANGED:
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
updateComponentSize();
repaint();
}
});
break;
case MISC_CHANGED:
repaint();
break;
case DISPLAYED_CHAIN_WILL_CHANGE:
default:
break;
}
}
@Override
public void operatorsChanged(ProcessRendererOperatorEvent e, Collection<Operator> operators) {
switch (e.getEventType()) {
case SELECTED_OPERATORS_CHANGED:
Operator firstOp = !operators.isEmpty() ? operators.iterator().next() : null;
if (firstOp != null) {
// only switch displayed chain if not in selected chain and selected op
// is not visible
OperatorChain displayChain = (OperatorChain) (firstOp instanceof ProcessRootOperator ? firstOp
: model.getDisplayedChain() == firstOp ? firstOp : firstOp.getParent());
if (displayChain != null && model.getDisplayedChain() != displayChain) {
model.setDisplayedChainAndFire(displayChain);
return;
}
}
repaint();
break;
case OPERATORS_MOVED:
for (Operator op : operators) {
boolean wasResized = controller.ensureProcessSizeFits(op.getExecutionUnit(),
model.getOperatorRect(op));
// need to repaint if process was not resized
if (!wasResized) {
repaint();
}
// notify registered listeners
fireOperatorMoved(op);
}
break;
case PORTS_CHANGED:
for (Operator op : operators) {
// trigger calculation of new op height
controller.processPortsChanged(op);
}
repaint();
break;
default:
break;
}
}
@Override
public void annotationsChanged(ProcessRendererAnnotationEvent e, Collection<WorkflowAnnotation> annotations) {
switch (e.getEventType()) {
case SELECTED_ANNOTATION_CHANGED:
repaint();
break;
case ANNOTATIONS_MOVED:
for (WorkflowAnnotation anno : annotations) {
boolean wasResized = controller.ensureProcessSizeFits(anno.getProcess(), anno.getLocation());
// need to repaint if process was not resized
if (!wasResized) {
repaint();
}
}
break;
case MISC_CHANGED:
repaint();
break;
default:
break;
}
}
});
// add GUI actions
renameAction = new RenameAction(this, controller);
selectAllAction = new SelectAllAction(this);
deleteSelectedAction = new DeleteSelectedConnectionAction(this);
arrangeOperatorsAction = new ArrangeOperatorsAction(this, controller);
autoFitAction = new AutoFitAction(controller);
// listen for process panel resizing events to adapt the render size
processPanel.addComponentListener(new ComponentAdapter() {
@Override
public void componentResized(final ComponentEvent e) {
super.componentResized(e);
controller.autoFit();
}
});
processPanel.addHierarchyListener(new HierarchyListener() {
@Override
public void hierarchyChanged(HierarchyEvent e) {
controller.autoFit();
}
});
// register transfer handler and drop target
setTransferHandler(new ProcessRendererTransferHandler(this, model, controller));
ProcessRendererDropTarget dropTarget;
try {
dropTarget = new ProcessRendererDropTarget(this, AbstractPatchedTransferHandler.getDropTargetListener());
setDropTarget(dropTarget);
model.setDropTargetSet(true);
} catch (Exception e) {
LogService.getRoot().log(Level.WARNING,
"com.rapidminer.gui.flow.processrendering.view.ProcessRendererView.drop_target_failed", e.getMessage());
}
// we need to know when the license changes because operators may become
// supported/unsupported
ProductConstraintManager.INSTANCE.registerLicenseManagerListener(new LicenseManagerListener() {
@Override
public <S, C> void handleLicenseEvent(final LicenseEvent<S, C> event) {
if (event.getType() == LicenseEventType.ACTIVE_LICENSE_CHANGED) {
ProcessRendererView.this.repaint();
}
}
});
// add some actions to the action map of this component
((ResourceAction) mainFrame.getActions().TOGGLE_BREAKPOINT[BreakpointListener.BREAKPOINT_AFTER]).addToActionMap(this,
WHEN_FOCUSED);
((ResourceAction) mainFrame.getActions().TOGGLE_ACTIVATION_ITEM).addToActionMap(this, WHEN_FOCUSED);
selectAllAction.addToActionMap(this, WHEN_FOCUSED);
OperatorTransferHandler.addToActionMap(this);
deleteSelectedAction.addToActionMap(this, "delete", WHEN_FOCUSED);
// add tooltips
new ToolTipWindow(new ProcessRendererTooltipProvider(model), this, TooltipLocation.RIGHT);
// add panning support to allow operators to extend and move the displayed process area when
// dragged to the side/bottom
new PanningManager(this);
init();
}
@Override
public void addNotify() {
super.addNotify();
// we do this here to avoid being overridden by main frame
renameAction.addToActionMap(this, WHEN_FOCUSED);
}
@Override
public void paintComponent(final Graphics graphics) {
super.paintComponent(graphics);
if (model.isDragStarted() || model.getConnectingPortSource() != null) {
((Graphics2D) graphics).setRenderingHints(ProcessDrawer.LOW_QUALITY_HINTS);
} else {
((Graphics2D) graphics).setRenderingHints(ProcessDrawer.HI_QUALITY_HINTS);
}
Graphics2D g2 = (Graphics2D) graphics.create();
getProcessDrawer().draw(g2, false);
g2.dispose();
}
@Override
public void printComponent(final Graphics graphics) {
((Graphics2D) graphics).setRenderingHints(ProcessDrawer.HI_QUALITY_HINTS);
getProcessDrawer().draw((Graphics2D) graphics.create(), true);
}
/**
* Return the index of the process under the given {@link Point2D view point}.
*
* @param p
* the point
* @return the index or -1 if no process is under the point
* @see #getProcessIndexOfOperator(Operator)
*/
public int getProcessIndexUnder(final Point2D p) {
if (p == null) {
return -1;
}
if (p.getY() < 0 || p.getY() > controller.getTotalHeight()) {
return -1;
}
int xOffset = 0;
for (int i = 0; i < model.getProcesses().size(); i++) {
int relativeX = (int) p.getX() - xOffset;
if (relativeX >= 0 && relativeX <= model.getProcessWidth(model.getProcess(i))) {
return i;
}
xOffset += ProcessDrawer.WALL_WIDTH * 2 + model.getProcessWidth(model.getProcess(i));
}
return -1;
}
/**
* Converts a {@link Point view point} to a point relative to the specified process.
*
* @param p
* the original point
* @param processIndex
* the index of the process
* @return the relative point or {@code null} if no process exists for the specified index
* @see ProcessRendererView#fromProcessSpace(Point, int)
*/
public Point toProcessSpace(final Point p, final int processIndex) {
if (processIndex == -1 || processIndex >= model.getProcesses().size()) {
return null;
}
int xOffset = getXOffset(processIndex);
double zoomFactor = model.getZoomFactor();
return new Point((int) ((p.getX() - xOffset) * (1 / zoomFactor)), (int) (p.getY() * (1 / zoomFactor)));
}
/**
* Returns the index of the process of the given {@link Operator}.
*
* @param op
* the operator
* @return the index or -1 if the process is not currently shown
* @since 7.5
* @see #getProcessIndexUnder(Point2D)
*/
public int getProcessIndexOfOperator(Operator op) {
if (op == null) {
return -1;
}
ExecutionUnit eu = op.getExecutionUnit();
return model.getProcessIndex(eu);
}
/**
* Converts a {@link Point} from the process to a point relative to the current view.
*
* @param p
* the process point
* @param processIndex
* the index of the process
* @return the view point or {@code null} if no process exists for the specified index
* @since 7.5
* @see #toProcessSpace(Point, int)
*/
public Point fromProcessSpace(final Point p, final int processIndex) {
if (processIndex == -1 || processIndex >= model.getProcesses().size()) {
return null;
}
int xOffset = getXOffset(processIndex);
double zoomFactor = model.getZoomFactor();
return new Point((int) (p.x * zoomFactor) + xOffset, (int) (p.y * zoomFactor));
}
/**
* Calculates the (absolute/view) x offset for the given process index. The index must be
* between 0 (inclusive) and the number of processes currently in the model (exclusive).
*
* @param processIndex
* the index of the process
* @return the x offset before the specified process
* @since 7.5
* @see #toProcessSpace(Point, int)
* @see #fromProcessSpace(Point, int)
*/
private int getXOffset(final int processIndex) {
List<ExecutionUnit> processes = model.getProcesses();
int xOffset = processIndex * ProcessDrawer.WALL_WIDTH * 2;
for (int i = 0; i < processIndex; i++) {
xOffset += model.getProcessWidth(processes.get(i));
}
return xOffset;
}
/**
* Call when the process has been updated, i.e. an operator has been added. Will update operator
* locations and then repaint.
*/
public void processUpdated() {
Operator hoveredOp = model.getHoveringOperator();
boolean hoveredOperatorFound = hoveredOp == null ? true : false;
List<Operator> movedOperators = new LinkedList<Operator>();
List<Operator> portChangedOperators = new LinkedList<Operator>();
// make sure location of every opterator is set and potentially reset hovered op
for (ExecutionUnit unit : model.getProcesses()) {
// check if all operators have positions, if not, set them now
movedOperators = controller.ensureOperatorsHaveLocation(unit);
for (Operator op : unit.getOperators()) {
// check if number of ports has changed for any operators
// otherwise we would not know that we need to check process size and repaint
Integer formerNumber = model.getNumberOfPorts(op);
Integer newNumber = op.getInputPorts().getNumberOfPorts() + op.getOutputPorts().getNumberOfPorts();
if (formerNumber == null || !newNumber.equals(formerNumber)) {
portChangedOperators.add(op);
model.setNumberOfPorts(op, newNumber);
}
// if hovered operator has not yet been found, see if current one is it
if (!hoveredOperatorFound && hoveredOp != null && hoveredOp.equals(op)) {
hoveredOperatorFound = true;
}
}
}
for (ExecutionUnit unit : model.getProcesses()) {
// check if number of ports has changed for any processes
// otherwise we would not know that we need to check process size and repaint
Operator op = unit.getEnclosingOperator();
Integer formerNumber = model.getNumberOfPorts(op);
Integer newNumber = op.getInputPorts().getNumberOfPorts() + op.getOutputPorts().getNumberOfPorts();
if (formerNumber == null || !newNumber.equals(formerNumber)) {
portChangedOperators.add(op);
model.setNumberOfPorts(op, newNumber);
}
}
// reset hovered operator if not in any process anymore
if (!hoveredOperatorFound) {
setHoveringOperator(null);
}
if (!movedOperators.isEmpty()) {
model.fireOperatorsMoved(movedOperators);
} else if (!portChangedOperators.isEmpty()) {
model.firePortsChanged(portChangedOperators);
}
}
@Override
public void repaint() {
if (repaintFilter != null) {
repaintFilter.requestRepaint();
} else {
doRepaint();
}
}
/**
* Does the repaint. Only call this method directly when there is a reason not to go through the
* {@link RepaintFilter}. Otherwise use {{@link #repaint()}.
*/
void doRepaint() {
super.repaint();
if (overviewPanel != null && overviewPanel.isShowing()) {
overviewPanel.repaint();
}
}
/**
* Adds a listener that will be informed when the user right-clicks an operator or a port.
*
* @param l
* the listener
*/
public void addProcessInteractionListener(final ProcessInteractionListener l) {
if (l == null) {
throw new IllegalArgumentException("l must not be null!");
}
processInteractionListeners.add(l);
}
/**
* @see #addProcessInteractionListener(ProcessInteractionListener)
*/
public void removeProcessInteractionListener(final ProcessInteractionListener l) {
if (l == null) {
throw new IllegalArgumentException("l must not be null!");
}
processInteractionListeners.remove(l);
}
/**
* Returns the {@link OverviewPanel} for this instance.
*
* @return the overview panel, never {@code null}
*/
public OverviewPanel getOverviewPanel() {
return overviewPanel;
}
@Override
public Component getExportComponent() {
return this;
}
@Override
public String getExportIconName() {
return I18N.getGUIMessage("gui.dockkey.process_panel.icon");
}
@Override
public String getExportName() {
return I18N.getGUIMessage("gui.dockkey.process_panel.name");
}
@Override
public String getIdentifier() {
Process process = RapidMinerGUI.getMainFrame().getProcess();
if (process != null) {
ProcessLocation processLocation = process.getProcessLocation();
if (processLocation != null) {
return processLocation.toString();
}
}
return null;
}
/**
* Returns the {@link DragListener} for the process renderer.
*
* @return the listener, never {@code null}
*/
public DragListener getDragListener() {
return dragListener;
}
/**
* Returns the {@link ProcessDrawer} which is responsible for drawing the process(es) in the
* {@link ProcessRendererView}.
*
* @return the drawer instance, never {@code null}
*/
public ProcessDrawer getProcessDrawer() {
return drawer;
}
/**
* Returns the {@link ProcessDrawer} which is responsible for drawing the process(es) in the
* {@link OverviewPanel}.
*
* @return the drawer instance, never {@code null}
*/
public ProcessDrawer getOverviewPanelDrawer() {
return drawerOverview;
}
/**
* Returns the {@link ProcessRendererModel} which is backing the GUI.
*
* @return the model instance, never {@code null}
*/
public ProcessRendererModel getModel() {
return model;
}
/**
* Returns the action which automatically fits the process size to the existing operator
* locations.
*
* @return the action, never {@code null}
*/
public Action getAutoFitAction() {
return autoFitAction;
}
/**
* Returns the action which automatically arranges all operators of the process according to a
* graph layout algorithm.
*
* @return the action, never {@code null}
*/
public Action getArrangeOperatorsAction() {
return arrangeOperatorsAction;
}
/**
* Adds the given renderer decorator for the specified render phase.
* <p>
* To add a {@link ProcessDrawDecorator}, call {@link #getProcessDrawer()} and
* {@link ProcessDrawer#addDecorator(ProcessDrawDecorator, RenderPhase)} on it.
* </p>
*
* @param decorator
* the decorator instance to add
* @param phase
* the phase during which the decorator should be notified of events. If multiple
* decorators want to handle events during the same phase, they are called in the
* order they were registered. If any of the decorators in the chain consume the
* event, the remaining decorators will <strong>not</strong> be notified!
*/
public void addEventDecorator(ProcessEventDecorator decorator, RenderPhase phase) {
if (decorator == null) {
throw new IllegalArgumentException("decorator must not be null!");
}
if (phase == null) {
throw new IllegalArgumentException("phase must not be null!");
}
decorators.get(phase).add(decorator);
}
/**
* Removes the given decorator for the specified render phase. If the decorator has already been
* removed, does nothing.
* <p>
* To remove a {@link ProcessDrawDecorator}, call {@link #getProcessDrawer()} and
* {@link ProcessDrawer#removeDecorator(ProcessDrawDecorator, RenderPhase)} on it.
* </p>
*
* @param decorator
* the decorator instance to remove
* @param phase
* the phase from which the decorator should be removed
*/
public void removeEventDecorator(ProcessEventDecorator decorator, RenderPhase phase) {
if (decorator == null) {
throw new IllegalArgumentException("decorator must not be null!");
}
if (phase == null) {
throw new IllegalArgumentException("phase must not be null!");
}
decorators.get(phase).remove(decorator);
}
/**
* Does the same as {@link ProcessDrawer#addDecorator(ProcessDrawDecorator, RenderPhase)}.
*
* @param decorator
* the draw decorator
* @param phase
* the specified phase in which to draw
*/
public void addDrawDecorator(ProcessDrawDecorator decorator, RenderPhase phase) {
getProcessDrawer().addDecorator(decorator, phase);
}
/**
* Does the same as {@link ProcessDrawer#removeDecorator(ProcessDrawDecorator, RenderPhase)}.
*
* @param decorator
* the draw decorator to add
* @param phase
* the specified phase for which the decorator was registered
*/
public void removeDrawDecorator(ProcessDrawDecorator decorator, RenderPhase phase) {
getProcessDrawer().removeDecorator(decorator, phase);
}
/**
* Does the same as {@link ProcessDrawer#addDecorator(OperatorDrawDecorator)}.
*
* @param decorator
* the operator draw decorator
*/
public void addDrawDecorator(OperatorDrawDecorator decorator) {
getProcessDrawer().addDecorator(decorator);
}
/**
* Does the same as {@link ProcessDrawer#removeDecorator(OperatorDrawDecorator)}.
*
* @param decorator
* the operator draw decorator to remove
*/
public void removeDrawDecorator(OperatorDrawDecorator decorator) {
getProcessDrawer().removeDecorator(decorator);
}
/**
* Opens a rename textfield at the location of the specified operator.
*
* @param op
* the operator to be renamed
*/
public void rename(final Operator op) {
if (op == null) {
throw new IllegalArgumentException("op must not be null!");
}
int processIndex = controller.getIndex(op.getExecutionUnit());
if (processIndex == -1) {
String name = SwingTools.showInputDialog("rename_operator", op.getName());
if (name != null && name.length() > 0) {
op.rename(name);
}
return;
}
renameField = new JTextField(10);
Rectangle2D rect = model.getOperatorRect(op);
int width = 0;
width = (int) ProcessDrawer.OPERATOR_FONT
.getStringBounds(op.getName(), ((Graphics2D) getGraphics()).getFontRenderContext()).getWidth();
width = Math.max(width, ProcessDrawer.OPERATOR_WIDTH);
double offset = (ProcessDrawer.OPERATOR_WIDTH - width) / 2;
renameField.setHorizontalAlignment(SwingConstants.CENTER);
renameField.setText(op.getName());
renameField.selectAll();
int x = (int) (rect.getX() * model.getZoomFactor());
int y = (int) (rect.getY() * model.getZoomFactor());
Point p = ProcessDrawUtils.convertToAbsoluteProcessPoint(new Point(x, y), processIndex, model);
int padding = 7;
renameField.setBounds((int) (p.getX() + offset - padding), (int) (p.getY() - 3), width + padding * 2, 21);
renameField.setFont(ProcessDrawer.OPERATOR_FONT);
renameField.setBorder(null);
add(renameField);
renameField.requestFocusInWindow();
// accepting changes on enter and focus lost
renameField.addActionListener(new ActionListener() {
@Override
public void actionPerformed(final ActionEvent e) {
if (renameField != null) {
String name = renameField.getText().trim();
if (name.length() > 0) {
op.rename(name);
}
remove(renameField);
renameField = null;
// this makes sure that pressing F2 afterwards works
// otherwise nothing might be focused
ProcessRendererView.this.requestFocusInWindow();
repaint();
}
}
});
renameField.addFocusListener(new FocusAdapter() {
@Override
public void focusLost(final FocusEvent e) {
// right-click menu
if (e.isTemporary()) {
return;
}
if (renameField != null) {
String name = renameField.getText().trim();
if (name.length() > 0) {
op.rename(name);
}
remove(renameField);
renameField = null;
// this makes sure that pressing F2 afterwards works
// otherwise nothing might be focused
ProcessRendererView.this.requestFocusInWindow();
repaint();
}
}
});
// ignore changes on escape
renameField.addKeyListener(new KeyAdapter() {
@Override
public void keyReleased(final KeyEvent e) {
if (e.getKeyCode() == KeyEvent.VK_ESCAPE) {
remove(renameField);
renameField = null;
// this makes sure that pressing F2 afterwards works
// otherwise nothing is focused until the next click
ProcessRendererView.this.requestFocusInWindow();
repaint();
}
}
});
repaint();
}
/**
* Updates location and size of the extension buttons.
*/
void updateExtensionButtons() {
for (ExtensionButton button : subprocessExtensionButtons) {
int subprocessIndex = button.getSubprocessIndex();
int buttonSize = button.getWidth();
int gap = 2 * ProcessDrawer.WALL_WIDTH;
if (subprocessIndex >= 0) {
Point location = ProcessDrawUtils.convertToAbsoluteProcessPoint(new Point(0, 0), subprocessIndex, model);
int height = (int) model.getProcessHeight(model.getProcess(subprocessIndex));
int width = (int) model.getProcessWidth(model.getProcess(subprocessIndex));
button.setBounds(location.x + width - buttonSize - gap - (button.isAdd() ? 0 : buttonSize),
location.y + height - gap - buttonSize, buttonSize, buttonSize);
} else {
Point location = ProcessDrawUtils.convertToAbsoluteProcessPoint(new Point(0, 0), 0, model);
int height = (int) model.getProcessHeight(model.getProcess(0));
button.setBounds(location.x + gap, location.y + height - gap - buttonSize, buttonSize, buttonSize);
}
}
}
/**
* Shows a popup menu if preconditions are fulfilled.
*
* @param e
* the mouse event potentially triggering the popup menu
* @return {@code true} if a popup menu was displayed; {@code false} otherwise
*/
boolean showPopupMenu(final MouseEvent e) {
if (model.getConnectingPortSource() != null) {
return false;
}
if (getProcessIndexUnder(e.getPoint()) == -1) {
return false;
}
JPopupMenu menu = new JPopupMenu();
// port or not port, that is the question
final Port hoveringPort = model.getHoveringPort();
if (hoveringPort != null) {
// add port actions
final IOObject data = hoveringPort.getAnyDataOrNull();
if (data != null && data instanceof ResultObject) {
JMenuItem showResult = new JMenuItem(
new ResourceAction(true, "show_port_data", ((ResultObject) data).getName()) {
private static final long serialVersionUID = -6557085878445788274L;
@Override
public void actionPerformed(final ActionEvent e) {
data.setSource(hoveringPort.getPorts().getOwner().getOperator().getName());
mainFrame.getResultDisplay().showResult((ResultObject) data);
}
});
menu.add(showResult);
try {
String locationString = mainFrame.getProcess().getRepositoryLocation().getAbsoluteLocation();
menu.add(new StoreInRepositoryAction(data, new RepositoryLocation(
locationString.substring(0, locationString.lastIndexOf(RepositoryLocation.SEPARATOR)))));
} catch (Exception e1) {
menu.add(new StoreInRepositoryAction(data));
}
menu.addSeparator();
}
List<QuickFix> fixes = hoveringPort.collectQuickFixes();
if (!fixes.isEmpty()) {
JMenu fixMenu = new ResourceMenu("quick_fixes");
for (QuickFix fix : fixes) {
fixMenu.add(fix.getAction());
}
menu.add(fixMenu);
}
if (hoveringPort.isConnected()) {
menu.add(new ResourceAction(true, "disconnect") {
private static final long serialVersionUID = 1L;
@Override
public void actionPerformed(final ActionEvent e) {
if (hoveringPort != null) {
if (hoveringPort.isConnected()) {
if (hoveringPort instanceof OutputPort) {
((OutputPort) hoveringPort).disconnect();
} else {
((InputPort) hoveringPort).getSource().disconnect();
}
}
}
}
});
}
if (model.getDisplayedChain() instanceof ProcessRootOperator) {
if (hoveringPort.getPorts() == model.getDisplayedChain().getSubprocess(0).getInnerSources()
|| hoveringPort.getPorts() == model.getDisplayedChain().getSubprocess(0).getInnerSinks()) {
menu.add(new ConnectPortToRepositoryAction(hoveringPort));
}
}
firePortMenuWillOpen(menu, hoveringPort);
} else if (model.getHoveringOperator() == null && model.getHoveringConnectionSource() != null) {
// right-clicked a connection spline
final Port port = model.getHoveringConnectionSource();
menu.add(new ResourceAction(true, "delete_connection") {
private static final long serialVersionUID = 1L;
@Override
public void actionPerformed(final ActionEvent e) {
if (port != null) {
if (port.isConnected()) {
OutputPort disconnectedPort;
if (port instanceof OutputPort) {
((OutputPort) port).disconnect();
disconnectedPort = (OutputPort) port;
} else {
((InputPort) port).getSource().disconnect();
disconnectedPort = ((InputPort) port).getSource();
}
if (port.equals(disconnectedPort)) {
model.setHoveringConnectionSource(null);
}
if (model.getSelectedConnectionSource() != null
&& model.getSelectedConnectionSource().equals(disconnectedPort)) {
model.setSelectedConnectionSource(null);
}
model.fireMiscChanged();
}
}
}
});
} else {
// add workflow annotation and background image actions
int index = model.getHoveringProcessIndex();
Action[] annotationActions = new Action[4];
final Operator hoveredOp = model.getHoveringOperator();
// reset zoom action if clicking on background and zoom is set
if (hoveredOp == null && model.getZoomFactor() != 1.0) {
menu.add(new ResourceAction(true, "processrenderer.reset_zoom") {
private static final long serialVersionUID = 1L;
@Override
public void actionPerformed(ActionEvent e) {
model.resetZoom();
model.fireProcessZoomChanged();
}
});
menu.addSeparator();
}
if (index != -1) {
ExecutionUnit process = model.getProcess(index);
Point point = toProcessSpace(e.getPoint(), index);
if (hoveredOp == null) {
annotationActions[0] = mainFrame.getProcessPanel().getAnnotationsHandler()
.makeAddProcessAnnotationAction(process, point);
annotationActions[1] = mainFrame.getProcessPanel().getAnnotationsHandler().getToggleAnnotationsAction();
annotationActions[2] = mainFrame.getProcessPanel().getBackgroundImageHandler()
.makeSetBackgroundImageAction(process);
if (model.getBackgroundImage(process) != null) {
annotationActions[3] = mainFrame.getProcessPanel().getBackgroundImageHandler()
.makeRemoveBackgroundImageAction(process);
}
} else {
WorkflowAnnotations annotations = model.getOperatorAnnotations(hoveredOp);
if (annotations == null || annotations.isEmpty()) {
annotationActions[0] = mainFrame.getProcessPanel().getAnnotationsHandler()
.makeAddOperatorAnnotationAction(hoveredOp);
} else {
annotationActions[0] = mainFrame.getProcessPanel().getAnnotationsHandler()
.makeDetachOperatorAnnotationAction(hoveredOp);
}
}
}
// add operator actions
mainFrame.getActions().addToOperatorPopupMenu(menu, renameAction, annotationActions);
// if not hovering on operator, add process panel actions
if (hoveredOp == null) {
menu.addSeparator();
menu.add(mainFrame.getProcessPanel().getFlowVisualizer().ALTER_EXECUTION_ORDER.createMenuItem());
JMenu layoutMenu = new ResourceMenu("process_layout");
layoutMenu.add(new ResourceAction("arrange_operators") {
private static final long serialVersionUID = 1L;
@Override
public void actionPerformed(final ActionEvent ae) {
int index = getProcessIndexUnder(e.getPoint());
if (index == -1) {
for (ExecutionUnit u : model.getProcesses()) {
controller.autoArrange(u);
}
} else {
controller.autoArrange(model.getProcess(index));
}
}
});
layoutMenu.add(autoFitAction);
menu.add(layoutMenu);
menu.addSeparator();
String name = "Process";
if (model.getDisplayedChain().getProcess().getProcessLocation() != null) {
name = model.getDisplayedChain().getProcess().getProcessLocation().getShortName();
}
menu.add(PrintingTools.makeExportPrintMenu(this, name));
fireOperatorMenuWillOpen(menu, model.getDisplayedChain());
} else {
boolean first = true;
for (OutputPort port : hoveredOp.getOutputPorts().getAllPorts()) {
final IOObject data = port.getAnyDataOrNull();
if (data != null && data instanceof ResultObject) {
if (first) {
menu.addSeparator();
first = false;
}
JMenuItem showResult = new JMenuItem(
new ResourceAction(true, "show_port_data", ((ResultObject) data).getName()) {
private static final long serialVersionUID = -6557085878445788274L;
@Override
public void actionPerformed(final ActionEvent e) {
data.setSource(hoveredOp.getName());
mainFrame.getResultDisplay().showResult((ResultObject) data);
}
});
menu.add(showResult);
}
}
fireOperatorMenuWillOpen(menu, hoveredOp);
}
}
// show popup
if (menu.getSubElements().length > 0) {
menu.show(this, e.getX(), e.getY());
}
return true;
}
/**
* Updates the currently displayed cursor depending on hover state.
*/
void updateCursor() {
if (model.getHoveringOperator() != null || model.getHoveringPort() != null) {
setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
} else {
setCursor(Cursor.getDefaultCursor());
}
}
/**
* Sets the hovering operator and updates the operator name rollout and the cursor.
*
* @param hoveringOperator
* the operator or {@code null}
*/
void setHoveringOperator(final Operator hoveringOperator) {
model.setHoveringOperator(hoveringOperator);
updateCursor();
model.fireMiscChanged();
}
/**
* Inits event listeners and GUI dimensions.
*/
private void init() {
addMouseMotionListener(mouseHandler);
addMouseListener(mouseHandler);
addMouseWheelListener(mouseHandler);
addKeyListener(keyHandler);
// for absolute positioning of tipPane
setLayout(null);
setPreferredSize(new Dimension(1000, 440));
setMinimumSize(new Dimension(100, 100));
setMaximumSize(new Dimension(2000, 2000));
}
/**
* Sets up the buttons which can be used to add/remove subprocesses of the currently displayed
* operator chain.
*/
private void setupExtensionButtons() {
for (ExtensionButton button : subprocessExtensionButtons) {
remove(button);
}
subprocessExtensionButtons.clear();
if (model.getDisplayedChain().areSubprocessesExtendable()) {
for (int index = 0; index < model.getProcesses().size(); index++) {
double width = model.getProcessWidth(model.getProcess(index)) + 1;
Point loc = ProcessDrawUtils.convertToAbsoluteProcessPoint(new Point(0, 0), index, model);
if (index == 0) {
ExtensionButton addButton2 = new ExtensionButton(model, model.getDisplayedChain(), -1, true);
addButton2.setBounds((int) (loc.getX() - addButton2.getPreferredSize().getWidth() + 1),
(int) (loc.getY() - 1), (int) addButton2.getPreferredSize().getWidth(),
(int) addButton2.getPreferredSize().getHeight());
subprocessExtensionButtons.add(addButton2);
add(addButton2);
}
ExtensionButton addButton = new ExtensionButton(model, model.getDisplayedChain(), index, true);
addButton.setBounds((int) (loc.getX() + width), (int) (loc.getY() - 1),
(int) addButton.getPreferredSize().getWidth(), (int) addButton.getPreferredSize().getHeight());
subprocessExtensionButtons.add(addButton);
add(addButton);
if (model.getProcesses().size() > 1) {
ExtensionButton deleteButton = new ExtensionButton(model, model.getDisplayedChain(), index, false);
deleteButton.setBounds((int) (loc.getX() + width), (int) (loc.getY() + addButton.getHeight() - 1),
(int) deleteButton.getPreferredSize().getWidth(),
(int) deleteButton.getPreferredSize().getHeight());
subprocessExtensionButtons.add(deleteButton);
add(deleteButton);
}
}
}
}
/**
* Update preferred size of this {@link JComponent} and updates the subprocess extension buttons
* as well.
*/
private void updateComponentSize() {
Dimension newSize = new Dimension((int) controller.getTotalWidth(), (int) controller.getTotalHeight());
updateExtensionButtons();
if (!newSize.equals(getPreferredSize())) {
setPreferredSize(newSize);
revalidate();
}
}
/**
* Notifies listeners that the operator context menu will be opened.
*
* @param m
* the menu instance
* @param op
* the operator for which the menu will open
*/
private void fireOperatorMenuWillOpen(final JPopupMenu m, final Operator op) {
List<ProcessInteractionListener> copy = new LinkedList<>(processInteractionListeners);
for (ProcessInteractionListener l : copy) {
l.operatorContextMenuWillOpen(m, op);
}
}
/**
* Notifies listeners that the port context menu will be opened.
*
* @param m
* the menu instance
* @param port
* the port for which the menu will open
*/
private void firePortMenuWillOpen(final JPopupMenu m, final Port port) {
List<ProcessInteractionListener> copy = new LinkedList<>(processInteractionListeners);
for (ProcessInteractionListener l : copy) {
l.portContextMenuWillOpen(m, port);
}
}
/**
* Notifies listeners that an operator has moved.
*
* @param op
* the operator that moved
*/
private void fireOperatorMoved(final Operator op) {
List<ProcessInteractionListener> copy = new LinkedList<>(processInteractionListeners);
for (ProcessInteractionListener l : copy) {
l.operatorMoved(op);
}
}
/**
* Notifies listeners that the displayed operator chain has changed.
*
* @param op
* the new displayed chain
*/
private void fireDisplayedChainChanged(final OperatorChain op) {
List<ProcessInteractionListener> copy = new LinkedList<>(processInteractionListeners);
for (ProcessInteractionListener l : copy) {
l.displayedChainChanged(op);
}
}
/**
* Lets all registered {@link ProcessEventDecorator}s process the mouse event for the given
* {@link RenderPhase}. If the event is consumed by any decorator, processing will stop.
*
* @param type
* the type of the mouse event
* @param e
* the event itself
* @param phase
* the event phase we are in
* @return {@code true} if the event was consumed; {@code false} otherwise
*/
private boolean processPhaseListenerMouseEvent(final MouseEventType type, final MouseEvent e, RenderPhase phase) {
int hoverIndex = model.getHoveringProcessIndex();
ExecutionUnit hoveredProcess = null;
if (hoverIndex >= 0 && hoverIndex < model.getProcesses().size()) {
hoveredProcess = model.getProcess(hoverIndex);
}
for (ProcessEventDecorator decorater : decorators.get(phase)) {
try {
decorater.processMouseEvent(hoveredProcess, type, e);
} catch (RuntimeException e1) {
// catch everything here
LogService.getRoot().log(Level.WARNING,
"com.rapidminer.gui.flow.processrendering.view.ProcessRendererView.decorator_error", e1);
}
// if the decorator consumed the event, it no longer makes sense to use it.
if (e.isConsumed()) {
return true;
}
}
return false;
}
/**
* Lets all registered {@link ProcessEventDecorator}s process the key event for the given
* {@link RenderPhase}. If the event is consumed by any decorator, processing will stop.
*
* @param type
* the type of the key event
* @param e
* the event itself
* @param phase
* the event phase we are in
* @return {@code true} if the event was consumed; {@code false} otherwise
*/
private boolean processPhaseListenerKeyEvent(final KeyEventType type, final KeyEvent e, RenderPhase phase) {
int hoverIndex = model.getHoveringProcessIndex();
ExecutionUnit hoveredProcess = null;
if (hoverIndex >= 0 && hoverIndex < model.getProcesses().size()) {
hoveredProcess = model.getProcess(hoverIndex);
}
for (ProcessEventDecorator decorater : decorators.get(phase)) {
try {
decorater.processKeyEvent(hoveredProcess, type, e);
} catch (RuntimeException e1) {
// catch everything here
LogService.getRoot().log(Level.WARNING,
"com.rapidminer.gui.flow.processrendering.view.ProcessRendererView.decorator_error", e1);
}
// if the decorator consumed the event, it no longer makes sense to use it.
if (e.isConsumed()) {
return true;
}
}
return false;
}
}