/**
* 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;
import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.geom.Rectangle2D;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.Collection;
import java.util.List;
import javax.swing.BorderFactory;
import javax.swing.JButton;
import javax.swing.JLayeredPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JViewport;
import javax.swing.SwingConstants;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import com.rapidminer.Process;
import com.rapidminer.gui.MainFrame;
import com.rapidminer.gui.actions.AutoWireAction;
import com.rapidminer.gui.flow.processrendering.annotations.AnnotationsVisualizer;
import com.rapidminer.gui.flow.processrendering.annotations.model.WorkflowAnnotation;
import com.rapidminer.gui.flow.processrendering.background.ProcessBackgroundImageVisualizer;
import com.rapidminer.gui.flow.processrendering.connections.RemoveHoveredConnectionDecorator;
import com.rapidminer.gui.flow.processrendering.connections.RemoveSelectedConnectionDecorator;
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.ProcessRendererView;
import com.rapidminer.gui.flow.processrendering.view.RenderPhase;
import com.rapidminer.gui.flow.processrendering.view.components.OperatorWarningHandler;
import com.rapidminer.gui.look.Colors;
import com.rapidminer.gui.processeditor.ProcessEditor;
import com.rapidminer.gui.tools.ExtendedJScrollPane;
import com.rapidminer.gui.tools.ResourceActionAdapter;
import com.rapidminer.gui.tools.ResourceDockKey;
import com.rapidminer.gui.tools.ViewToolBar;
import com.rapidminer.operator.Operator;
import com.rapidminer.operator.OperatorChain;
import com.vlsolutions.swing.docking.DockKey;
import com.vlsolutions.swing.docking.Dockable;
/**
* Contains the main {@link ProcessRendererView} and a {@link ProcessButtonBar} to navigate through
* the process.
*
* @author Simon Fischer, Tobias Malbrecht, Jan Czogalla
*/
public class ProcessPanel extends JPanel implements Dockable, ProcessEditor {
/**
* A helper class to scroll to the view position of the currently displayed operator chain, e.g.
* after the displayed chain has changed.
*
* @author Jan Czogalla
* @see ProcessPanel#scrollToViewPosition(Point)
* @see ProcessPanel#scrollToProcessPosition(Point, int)
* @see ProcessPanel#scrollToOperator(Operator)
* @since 7.5
*/
private final class Scroller implements ChangeListener, PropertyChangeListener {
private boolean drawn = false;
private boolean running = false;
private Operator op;
@Override
public void stateChanged(ChangeEvent e) {
if (running) {
return;
}
getViewPort().removeChangeListener(this);
propertyChange(null);
}
private void scroll() {
ProcessRendererModel model = renderer.getModel();
OperatorChain opChain = model.getDisplayedChain();
if (opChain == null) {
return;
}
if (op != null && op != opChain) {
// scroll to operator after selection
// force centering (happened after displayed chain changed)
scrollToOperator(op, true);
drawn = true;
return;
}
Point position = model.getScrollPosition(opChain);
int index = -1;
if (position != null) {
model.resetScrollPosition(opChain);
Double i = model.getScrollIndex(opChain);
if (i != null) {
model.resetScrollIndex(opChain);
index = i.intValue();
}
} else {
position = model.getOperatorChainPosition(opChain);
}
if (position == null) {
position = new Point();
}
if (index == -1) {
// scroll to zoom-specific position (e.g. re-entering the current chain)
scrollToViewPosition(position);
} else {
// scroll to process-specific position (e.g. after zooming)
scrollToProcessPosition(position, index);
}
drawn = true;
}
public void setOperator(Operator op) {
this.op = op;
}
public boolean isFinished() {
return drawn;
}
@Override
public synchronized void propertyChange(PropertyChangeEvent evt) {
running = true;
getViewPort().removePropertyChangeListener(SCROLLER_UPDATE, this);
scroll();
running = false;
}
}
private static final long serialVersionUID = -4419160224916991497L;
public static final String SCROLLER_UPDATE = "rm.scroller_update";
/** the process renderer instance */
private final ProcessRendererView renderer;
/** the flow visualizer instance */
private final FlowVisualizer flowVisualizer;
/** the workflow annotations handler instance */
private final AnnotationsVisualizer annotationsHandler;
/** the background image handler */
private final ProcessBackgroundImageVisualizer backgroundImageHandler;
/** the handler for operator warning bubbles */
private final OperatorWarningHandler operatorWarningHandler;
private final ProcessButtonBar processButtonBar;
private final JButton resetZoom;
private final JButton zoomIn;
private final JButton zoomOut;
private OperatorChain operatorChain;
private final JScrollPane scrollPane;
public ProcessPanel(final MainFrame mainFrame) {
setOpaque(true);
setBackground(Colors.PANEL_BACKGROUND);
processButtonBar = new ProcessButtonBar(mainFrame);
final ProcessRendererModel model = new ProcessRendererModel();
// listen for display chain changes and update breadcrumbs accordingly
model.registerEventListener(new ProcessRendererEventListener() {
@Override
public void operatorsChanged(ProcessRendererOperatorEvent e, Collection<Operator> operators) {
// don't care
}
@Override
public void modelChanged(ProcessRendererModelEvent e) {
switch (e.getEventType()) {
case DISPLAYED_CHAIN_CHANGED:
case DISPLAYED_PROCESSES_CHANGED:
processButtonBar.setSelectedNode(renderer.getModel().getDisplayedChain());
break;
case PROCESS_ZOOM_CHANGED:
zoomIn.setEnabled(model.canZoomIn());
zoomOut.setEnabled(model.canZoomOut());
resetZoom.setText((int) (model.getZoomFactor() * 100) + "%");
break;
case PROCESS_SIZE_CHANGED:
case MISC_CHANGED:
case DISPLAYED_CHAIN_WILL_CHANGE:
default:
break;
}
}
@Override
public void annotationsChanged(ProcessRendererAnnotationEvent e, Collection<WorkflowAnnotation> annotations) {
// don't care
}
});
renderer = new ProcessRendererView(model, this, mainFrame);
// listen for operator selection and view changes (displayed chain/zoom) to adjust view
// position as needed
model.registerEventListener(new ProcessRendererEventListener() {
Scroller scroller;
@Override
public void operatorsChanged(ProcessRendererOperatorEvent e, Collection<Operator> operators) {
switch (e.getEventType()) {
case SELECTED_OPERATORS_CHANGED:
Operator operator = operators.iterator().next();
Rectangle2D opRect = model.getOperatorRect(operator);
OperatorChain parent = operator.getParent();
if (opRect == null || parent == null || model.getDisplayedChain() != parent) {
break;
}
if (model.getRestore(operator)) {
// don't scroll further after undo/redo
model.resetRestore(operator);
break;
}
if (operators.size() != 1) {
// only scroll to operator if it is the single one selected;
// there is no sensible behavior for scrolling to multiple operators yet
return;
}
if (scroller != null && !scroller.isFinished()) {
// scroll to operator after new chain is painted
scroller.setOperator(operator);
} else {
// scroll to operator directly (chain is displayed)
scrollToOperator(operator);
}
break;
case OPERATORS_MOVED:
case PORTS_CHANGED:
default:
break;
}
}
@Override
public void modelChanged(ProcessRendererModelEvent e) {
switch (e.getEventType()) {
case DISPLAYED_CHAIN_WILL_CHANGE:
// before change: save zoom lvl & center point
{
OperatorChain opChain = model.getDisplayedChain();
if (opChain == null) {
return;
}
// always save zoom, even if no scrollbars exist
model.setOperatorChainZoom(opChain, model.getZoomFactor());
// only need to save scroll position if scrollbars exist
if (scroller != null && !scroller.isFinished()) {
return;
} else {
scroller = null;
}
Point position = getCurrentViewCenter();
model.setOperatorChainPosition(opChain, position);
break;
}
case DISPLAYED_CHAIN_CHANGED:
// after change: restore zoom lvl first
OperatorChain opChain = model.getDisplayedChain();
if (opChain == null) {
break;
}
Double zoom = model.getOperatorChainZoom(opChain);
if (zoom != null) {
if (zoom != model.getZoomFactor()) {
model.setZoomFactor(zoom);
model.fireProcessZoomChanged();
}
} else if (model.getZoomFactor() != 1) {
model.resetZoom();
model.fireProcessZoomChanged();
}
//$FALL-THROUGH$
case PROCESS_ZOOM_CHANGED:
if (scroller == null || scroller.isFinished()) {
// scroll to position as soon as view port has adjusted
getViewPort().addChangeListener(scroller = new Scroller());
getViewPort().addPropertyChangeListener(SCROLLER_UPDATE, scroller);
}
break;
case DISPLAYED_PROCESSES_CHANGED:
case MISC_CHANGED:
case PROCESS_SIZE_CHANGED:
default:
break;
}
}
@Override
public void annotationsChanged(ProcessRendererAnnotationEvent e, Collection<WorkflowAnnotation> annotations) {
// don't care
}
});
flowVisualizer = new FlowVisualizer(renderer);
annotationsHandler = new AnnotationsVisualizer(renderer, flowVisualizer);
backgroundImageHandler = new ProcessBackgroundImageVisualizer(renderer);
RemoveSelectedConnectionDecorator removeSelectedConnectionDecorator = new RemoveSelectedConnectionDecorator(
renderer.getModel());
renderer.addDrawDecorator(removeSelectedConnectionDecorator, RenderPhase.CONNECTIONS);
renderer.addEventDecorator(removeSelectedConnectionDecorator, RenderPhase.CONNECTIONS);
RemoveHoveredConnectionDecorator removeHoveredConnectionDecorator = new RemoveHoveredConnectionDecorator(
renderer.getModel());
renderer.addDrawDecorator(removeHoveredConnectionDecorator, RenderPhase.CONNECTIONS);
// event decorator must be in phase OVERLAY such that it comes before selecting of
// connections which is done in between phases OPERATOR_ADDITIONS and OPERATORS
renderer.addEventDecorator(removeHoveredConnectionDecorator, RenderPhase.OVERLAY);
ViewToolBar toolBar = new ViewToolBar(ViewToolBar.LEFT);
zoomIn = new JButton(new ResourceActionAdapter(true, "processpanel.zoom_in") {
private static final long serialVersionUID = 1L;
@Override
public void actionPerformed(ActionEvent e) {
fireProcessZoomWillChange();
model.zoomIn();
model.fireProcessZoomChanged();
}
});
zoomOut = new JButton(new ResourceActionAdapter(true, "processpanel.zoom_out") {
private static final long serialVersionUID = 1L;
@Override
public void actionPerformed(ActionEvent e) {
fireProcessZoomWillChange();
model.zoomOut();
model.fireProcessZoomChanged();
}
});
resetZoom = new JButton(new ResourceActionAdapter(true, "processpanel.reset_zoom") {
private static final long serialVersionUID = 1L;
@Override
public void actionPerformed(ActionEvent e) {
fireProcessZoomWillChange();
model.resetZoom();
model.fireProcessZoomChanged();
}
});
resetZoom.setHorizontalTextPosition(SwingConstants.LEADING);
toolBar.add(resetZoom);
toolBar.add(zoomIn);
toolBar.add(zoomOut);
toolBar.add(annotationsHandler.makeAddAnnotationAction(null), ViewToolBar.RIGHT);
toolBar.add(new AutoWireAction(mainFrame), ViewToolBar.RIGHT);
toolBar.add(flowVisualizer.SHOW_ORDER_TOGGLEBUTTON, ViewToolBar.RIGHT);
toolBar.add(renderer.getAutoFitAction(), ViewToolBar.RIGHT);
setLayout(new BorderLayout());
JLayeredPane processLayeredPane = new JLayeredPane();
processLayeredPane.setLayout(new BorderLayout());
processLayeredPane.add(processButtonBar, BorderLayout.WEST, 1);
processLayeredPane.add(toolBar, BorderLayout.EAST, 0);
processLayeredPane.setBorder(BorderFactory.createEmptyBorder(5, 0, 5, 0));
add(processLayeredPane, BorderLayout.NORTH);
scrollPane = new ExtendedJScrollPane(renderer);
scrollPane.getVerticalScrollBar().setUnitIncrement(10);
scrollPane.setBorder(BorderFactory.createMatteBorder(1, 0, 0, 0, Colors.TEXTFIELD_BORDER));
add(scrollPane, BorderLayout.CENTER);
new ProcessPanelScroller(renderer, scrollPane);
// add event decorator for operator warning icons
operatorWarningHandler = new OperatorWarningHandler(model);
renderer.addEventDecorator(operatorWarningHandler, RenderPhase.OPERATOR_ADDITIONS);
}
/**
* Fires update before zoom changes, keeping the view centered
*
* @see ProcessRendererModel#prepareProcessZoomWillChange(Point, int)
* @since 7.5
*/
private void fireProcessZoomWillChange() {
Point center = getCurrentViewCenter();
int index = renderer.getProcessIndexUnder(center);
center = renderer.toProcessSpace(center, index);
renderer.getModel().prepareProcessZoomWillChange(center, index);
}
/**
* Shows the specified {@link OperatorChain} in the {@link ProcessRendererView}.
*
* @param operatorChain
* the operator chain to show, must not be {@code null}
* @deprecated use {@link ProcessRendererModel#setDisplayedChain(OperatorChain)} and
* {@link ProcessRendererModel#fireDisplayedChainChanged()} instead
*/
@Deprecated
public void showOperatorChain(OperatorChain operatorChain) {
if (operatorChain == null) {
throw new IllegalArgumentException("operatorChain must not be null!");
}
this.operatorChain = operatorChain;
renderer.getModel().setDisplayedChainAndFire(operatorChain);
}
@Override
public void setSelection(List<Operator> selection) {
Operator first = selection.isEmpty() ? null : selection.get(0);
if (first != null) {
processButtonBar.addToHistory(first);
}
}
@Override
public void processChanged(Process process) {
processButtonBar.clearHistory();
}
@Override
public void processUpdated(Process process) {
renderer.processUpdated();
if (operatorChain != null) {
processButtonBar.setSelectedNode(operatorChain);
}
}
/**
* The {@link ProcessRendererView} which is responsible for displaying the current process as
* well as interaction with it
*
* @return the instance, never {@code null}
*/
public ProcessRendererView getProcessRenderer() {
return renderer;
}
/**
* The {@link FlowVisualizer} instance tied to the process renderer.
*
* @return the instance, never {@code null}
*/
public FlowVisualizer getFlowVisualizer() {
return flowVisualizer;
}
/**
* The {@link AnnotationsVisualizer} instance tied to the process renderer.
*
* @return the instance, never {@code null}
*/
public AnnotationsVisualizer getAnnotationsHandler() {
return annotationsHandler;
}
/**
* The {@link ProcessBackgroundImageVisualizer} instance tied to the process renderer.
*
* @return the instance, never {@code null}
*/
public ProcessBackgroundImageVisualizer getBackgroundImageHandler() {
return backgroundImageHandler;
}
public static final String PROCESS_PANEL_DOCK_KEY = "process_panel";
private final DockKey DOCK_KEY = new ResourceDockKey(PROCESS_PANEL_DOCK_KEY);
{
DOCK_KEY.setDockGroup(MainFrame.DOCK_GROUP_ROOT);
}
@Override
public Component getComponent() {
return this;
}
@Override
public DockKey getDockKey() {
return DOCK_KEY;
}
public JViewport getViewPort() {
return scrollPane.getViewport();
}
/** Returns the center position of the current view. */
public Point getCurrentViewCenter() {
Rectangle viewRect = getViewPort().getViewRect();
Point center = new Point((int) viewRect.getCenterX(), (int) viewRect.getCenterY());
return center;
}
/**
* Scrolls the view to the specified {@link Operator}, making it visible. Will not force
* centering
*
* @param operator
* the operator to focus on
* @return whether the scrolling was successful
* @since 7.5
* @see #scrollToOperator(Operator, boolean)
*/
public boolean scrollToOperator(Operator operator) {
return scrollToOperator(operator, false);
}
/**
* Scrolls the view to the specified {@link Operator}, making it the center of the view if
* necessary, indicated by the flag.
*
* @param operator
* the operator to focus on
* @param toCenter
* flag to indicate whether to force centering
* @return whether the scrolling was successful
* @since 7.5
* @see #scrollToViewPosition(Point)
*/
public boolean scrollToOperator(Operator operator, boolean toCenter) {
Rectangle2D opRect = renderer.getModel().getOperatorRect(operator);
if (opRect == null) {
return false;
}
int pIndex = renderer.getProcessIndexOfOperator(operator);
if (pIndex == -1) {
return false;
}
Rectangle opViewRect = getOpViewRect(opRect, pIndex);
Rectangle viewRect = getViewPort().getViewRect();
if (!toCenter) {
if (viewRect.contains(opViewRect)) {
// if operator visible, do nothing
return false;
}
if (viewRect.intersects(opViewRect)) {
// if partially visible, just scroll it into view
opViewRect.translate(-viewRect.x, -viewRect.y);
getViewPort().scrollRectToVisible(opViewRect);
// return false nonetheless, see PortInfoBubble
return false;
}
}
Point opCenter = new Point((int) opViewRect.getCenterX(), (int) opViewRect.getCenterY());
scrollToViewPosition(opCenter);
return true;
}
/**
* Calculates the view rectangle of the given process rectangle, adding in some border padding.
*
* @param opRect
* the operator rectangle in the process
* @param pIndex
* the process index of the corresponding operator
* @return the view rectangle
* @since 7.5
*/
private Rectangle getOpViewRect(Rectangle2D opRect, int pIndex) {
Rectangle target = new Rectangle();
target.setLocation(renderer.fromProcessSpace(opRect.getBounds().getLocation(), pIndex));
double zoomFactor = renderer.getModel().getZoomFactor();
target.setSize((int) (opRect.getWidth() * zoomFactor), (int) (opRect.getHeight() * zoomFactor));
target.grow(ProcessDrawer.PORT_SIZE, ProcessDrawer.WALL_WIDTH * 2);
return target;
}
/**
* Scrolls the view to the specified {@link Point center point}.
*
* @param center
* the point to focus on
* @since 7.5
* @see #scrollToProcessPosition(Point)
*/
public void scrollToViewPosition(Point center) {
getViewPort().scrollRectToVisible(getScrollRectangle(center));
}
/**
* Scrolls the view to the specified {@link Point process point}.
*
* @param center
* the point to focus on
* @param processIndex
* the index of the process to focus on
* @since 7.5
* @see #scrollToViewPosition(Point)
*/
public void scrollToProcessPosition(Point center, int processIndex) {
scrollToViewPosition(renderer.fromProcessSpace(center, processIndex));
}
/**
* Calculates the relative scroll rectangle from the current view, so that the specified
* {@link Point} is in the center if possible.
*
* @param center
* the point to focus on
* @return the relative scroll rectangle
* @since 7.5
*/
private Rectangle getScrollRectangle(Point center) {
Point newViewPoint = new Point(center);
Rectangle currentViewRect = getViewPort().getViewRect();
newViewPoint.translate((int) -currentViewRect.getCenterX(), (int) -currentViewRect.getCenterY());
return new Rectangle(newViewPoint, currentViewRect.getSize());
}
/**
* Returns the handler for operator warning bubbles.
*
* @return the handler for operator warnings, never {@code null}
*/
public OperatorWarningHandler getOperatorWarningHandler() {
return operatorWarningHandler;
}
}