/** * 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.draw; import java.awt.BasicStroke; import java.awt.Color; import java.awt.Font; import java.awt.Graphics2D; import java.awt.Paint; import java.awt.Point; import java.awt.Rectangle; import java.awt.RenderingHints; import java.awt.Shape; import java.awt.Stroke; import java.awt.geom.AffineTransform; import java.awt.geom.Arc2D; import java.awt.geom.Line2D; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; import java.awt.geom.RoundRectangle2D; import java.util.Collections; 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.ImageIcon; import com.rapidminer.BreakpointListener; import com.rapidminer.Process; import com.rapidminer.gui.MainFrame; import com.rapidminer.gui.RapidMinerGUI; import com.rapidminer.gui.RapidMinerGUI.DragHighlightMode; import com.rapidminer.gui.animation.Animation; import com.rapidminer.gui.animation.ProcessAnimationManager; 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.look.Colors; import com.rapidminer.gui.look.RapidLookAndFeel; import com.rapidminer.gui.tools.SwingTools; import com.rapidminer.io.process.GUIProcessXMLFilter; import com.rapidminer.operator.ExecutionUnit; import com.rapidminer.operator.Operator; import com.rapidminer.operator.OperatorChain; import com.rapidminer.operator.ProcessRootOperator; import com.rapidminer.operator.ports.InputPort; import com.rapidminer.operator.ports.InputPorts; import com.rapidminer.operator.ports.OutputPort; import com.rapidminer.operator.ports.OutputPorts; import com.rapidminer.operator.ports.Port; import com.rapidminer.operator.ports.Ports; import com.rapidminer.operator.ports.metadata.CollectionMetaData; import com.rapidminer.operator.ports.metadata.Precondition; import com.rapidminer.tools.I18N; import com.rapidminer.tools.LogService; import com.rapidminer.tutorial.Tutorial; /** * This class does the actual Java2D drawing for the {@link ProcessRendererView}. * * @author Marco Boeck * @since 6.4.0 * */ public final class ProcessDrawer { public static final Font OPERATOR_FONT = ProcessRendererModel.OPERATOR_FONT; public static final int OPERATOR_WIDTH = ProcessRendererModel.OPERATOR_WIDTH; public static final int OPERATOR_MIN_HEIGHT = ProcessRendererModel.MIN_OPERATOR_HEIGHT; public static final int HEADER_HEIGHT = ProcessRendererModel.HEADER_HEIGHT; public static final int PORT_SIZE = ProcessRendererModel.PORT_SIZE; public static final int PORT_SIZE_HIGHLIGHT = (int) (PORT_SIZE * 1.4f); public static final int PORT_OFFSET = OPERATOR_FONT.getSize() + 6 + PORT_SIZE; public static final int WALL_WIDTH = 3; public static final int GRID_WIDTH = OPERATOR_WIDTH * 3 / 4; public static final int GRID_HEIGHT = OPERATOR_MIN_HEIGHT * 3 / 4; public static final int GRID_X_OFFSET = OPERATOR_WIDTH / 2; public static final int GRID_Y_OFFSET = OPERATOR_MIN_HEIGHT / 2; public static final int GRID_AUTOARRANGE_WIDTH = OPERATOR_WIDTH * 3 / 2; public static final int GRID_AUTOARRANGE_HEIGHT = OPERATOR_MIN_HEIGHT * 3 / 2; /** rendering hints which enable general and text anti aliasing */ public static final RenderingHints HI_QUALITY_HINTS = new RenderingHints(null); /** rendering hints which disable general and text anti aliasing */ public static final RenderingHints LOW_QUALITY_HINTS = new RenderingHints(null); static { HI_QUALITY_HINTS.put(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); HI_QUALITY_HINTS.put(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); LOW_QUALITY_HINTS.put(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); LOW_QUALITY_HINTS.put(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); } public static final Color INNER_DRAG_COLOR = new Color(230, 242, 255, 150); public static final Color BORDER_DRAG_COLOR = new Color(200, 200, 200, 200); public static final Color OPERATOR_BORDER_COLOR_SELECTED = new Color(255, 102, 0); private static final Color INNER_COLOR = Color.WHITE; private static final Color SHADOW_COLOR = Color.LIGHT_GRAY; private static final int OPERATOR_CORNER = (int) (RapidLookAndFeel.CORNER_DEFAULT_RADIUS * 0.67); private static final int OPERATOR_BG_CORNER = RapidLookAndFeel.CORNER_DEFAULT_RADIUS; private static final Color BORDER_COLOR = Colors.TAB_BORDER; private static final Color HINT_COLOR = new Color(230, 230, 230); private static final Color LINE_COLOR = Color.DARK_GRAY; private static final Stroke LINE_STROKE = new BasicStroke(1f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND); private static final Stroke PORT_STROKE = new BasicStroke(1.5f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND); private static final Stroke PORT_HIGHLIGHT_STROKE = new BasicStroke(2f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND); private static final Stroke SELECTION_RECT_STROKE = new BasicStroke(1f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND, 5f, new float[] { 2f, 2f }, 0f); private static final Paint SELECTION_RECT_PAINT = Color.GRAY; private static final Color PROCESS_TITLE_COLOR = SHADOW_COLOR; private static final Stroke BORDER_DRAG_STROKE = new BasicStroke(1.5f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_MITER, 10.0f, new float[] { 10.0f }, 0.0f); private static final int DRAG_BORDER_PADDING = 30; private static final int DRAG_BORDER_CORNER = 15; private static final Font PROCESS_FONT = new Font("Dialog", Font.BOLD, 12); private static final Font DRAG_FONT_LARGE = new Font("Dialog", Font.BOLD, 30); private static final Font DRAG_FONT_MEDIUM = new Font("Dialog", Font.BOLD, 22); private static final Font DRAG_FONT_SMALL = new Font("Dialog", Font.BOLD, 14); private static final Font HINT_FONT_LARGE = new Font("Dialog", Font.BOLD, 30); private static final Font HINT_FONT_MEDIUM = new Font("Dialog", Font.BOLD, 24); private static final Font HINT_FONT_SMALL = new Font("Dialog", Font.BOLD, 18); private static final Font PORT_FONT = new Font("Dialog", Font.PLAIN, 9); private static final Color PORT_NAME_COLOR = Color.DARK_GRAY; private static final Color PORT_NAME_SELECTION_COLOR = Color.GRAY; private static final Color ACTIVE_EDGE_COLOR = new Color(255, 102, 0); private static final Stroke OPERATOR_STROKE_SELECTED = new BasicStroke(1.5f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND); private static final Stroke OPERATOR_STROKE_HIGHLIGHT = new BasicStroke(1.25f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND); private static final Stroke OPERATOR_STROKE_NORMAL = new BasicStroke(1f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND); private static final Color OPERATOR_BORDER_COLOR = new Color(129, 129, 129); private static final Color OPERATOR_BORDER_COLOR_HIGHLIGHT = new Color(100, 100, 100); private static final Color OPERATOR_NAME_COLOR = new Color(51, 51, 51); private static final Color OPERATOR_NAME_COLOR_HIGHLIGHT = new Color(85, 85, 85); private static final Color OPERATOR_NAME_COLOR_SELECTED = OPERATOR_BORDER_COLOR_SELECTED; private static final Color OPERATOR_NAME_COLOR_DISABLED = new Color(150, 150, 150); private static final Stroke CONNECTION_LINE_STROKE = new BasicStroke(1.3f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND); private static final Stroke CONNECTION_HIGHLIGHT_STROKE = new BasicStroke(2.2f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND); private static final Stroke CONNECTION_LINE_BACKGROUND_STROKE = new BasicStroke(7f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND); private static final Stroke CONNECTION_COLLECTION_LINE_STROKE = new BasicStroke(3f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND); private static final Stroke CONNECTION_COLLECTION_HIGHLIGHT_STROKE = new BasicStroke(4f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND); private static final Stroke CONNECTION_COLLECTION_LINE_BACKGROUND_STROKE = new BasicStroke(12f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND); private static final String HINT_EMPTY_PROCESS_1 = I18N.getGUIMessage("gui.label.processRenderer.empty_hint_1.label"); private static final String HINT_EMPTY_PROCESS_2 = I18N.getGUIMessage("gui.label.processRenderer.empty_hint_2.label"); private static final String HINT_EMPTY_PROCESS_3 = I18N.getGUIMessage("gui.label.processRenderer.empty_hint_3.label"); private static final String DRAG_HERE = I18N.getGUIMessage("gui.label.processRenderer.drag_here.label"); private static final String DROP_HERE = I18N.getGUIMessage("gui.label.processRenderer.drop_here.label"); private static final Color DRAG_BG_COLOR = BORDER_DRAG_COLOR; private static final Color DRAG_FG_COLOR = new Color(254, 254, 254); private static final int PROCESS_TITLE_PADDING = 10; private static final double PORT_SIZE_BACKGROUND = PORT_SIZE * 2f; private static final double MAX_HEADER_RATIO = 1.35; private static final ImageIcon IMAGE_WARNING = SwingTools.createIcon("16/sign_warning2.png"); private static final ImageIcon IMAGE_BREAKPOINT_WITHIN = SwingTools.createIcon("16/breakpoint.png"); private static final ImageIcon IMAGE_BREAKPOINTS = SwingTools.createIcon("16/breakpoints.png"); private static final ImageIcon IMAGE_BREAKPOINT_BEFORE = SwingTools.createIcon("16/breakpoint_left.png"); private static final ImageIcon IMAGE_BREAKPOINT_AFTER = SwingTools.createIcon("16/breakpoint_right.png"); private static final ImageIcon IMAGE_BREAKPOINT_WITHIN_LARGE = SwingTools.createIcon("24/breakpoint.png"); private static final ImageIcon IMAGE_BREAKPOINTS_LARGE = SwingTools.createIcon("24/breakpoints.png"); private static final ImageIcon IMAGE_BREAKPOINT_BEFORE_LARGE = SwingTools.createIcon("24/breakpoint_left.png"); private static final ImageIcon IMAGE_BREAKPOINT_AFTER_LARGE = SwingTools.createIcon("24/breakpoint_right.png"); private static final ImageIcon IMAGE_BRANCH = SwingTools.createIcon("16/elements_tree.png"); private static final ImageIcon OPERATOR_RUNNING = SwingTools.createIcon("16/media_play2.png"); private static final ImageIcon OPERATOR_READY = SwingTools.createIcon("16/check.png"); /** the size of the operator icon */ private static final int OPERATOR_ICON_SIZE = 24; /** the model backing the process renderer view */ private ProcessRendererModel model; /** indicates if drop highlighting should be rendered */ private boolean drawHighlight; /** the list of draw decorators */ private Map<RenderPhase, CopyOnWriteArrayList<ProcessDrawDecorator>> decorators; /** the list of operator decorators */ private CopyOnWriteArrayList<OperatorDrawDecorator> operatorDecorators; /** * Creates a new drawer instance which can be used to draw the process specified in the model. * * @param model * the model containing the data needed to draw the process. See * {@link ProcessRendererModel} for a minimal configuration * @param drawHighlight * if {@code true} will highlight drop area in the process during drag & drop */ public ProcessDrawer(final ProcessRendererModel model, final boolean drawHighlight) { if (model == null) { throw new IllegalArgumentException("model must not be null!"); } this.model = model; this.drawHighlight = drawHighlight; // prepare decorators for each phase decorators = new HashMap<>(); for (RenderPhase phase : RenderPhase.drawOrder()) { decorators.put(phase, new CopyOnWriteArrayList<ProcessDrawDecorator>()); } // prepare operator decorators operatorDecorators = new CopyOnWriteArrayList<OperatorDrawDecorator>(); } /** * Draws the entire process(es) based on the {@link ProcessRendererModel} data. Registered * decorators are called during their respective {@link RenderPhase}s. * * @param g2 * the graphics context to draw upon * @param printing * if {@code true} we are printing instead of drawing to the screen */ public void draw(final Graphics2D g2, final boolean printing) { Graphics2D g = (Graphics2D) g2.create(); double zoomFactor = model.getZoomFactor(); // draw every process for (ExecutionUnit process : model.getProcesses()) { // draw background in own graphics context to avoid cross-phase manipulations Graphics2D gBG = (Graphics2D) g.create(); drawBackground(process, gBG, printing); gBG.dispose(); // remember non-scaled transform AffineTransform at = g.getTransform(); g.scale(zoomFactor, zoomFactor); // let decorators draw Graphics2D gBGD = (Graphics2D) g.create(); drawPhaseDecorators(process, gBGD, RenderPhase.BACKGROUND, printing); gBGD.dispose(); // draw annotations in own graphics context to avoid cross-phase manipulations Graphics2D gAN = (Graphics2D) g.create(); drawAnnotations(process, gAN, printing); gAN.dispose(); // draw operator annotations in own graphics context to avoid cross-phase manipulations Graphics2D gON = (Graphics2D) g.create(); drawOperatorAnnotations(process, gON, printing); gON.dispose(); // draw operator backgrounds in own graphics context to avoid cross-phase manipulations Graphics2D gOB = (Graphics2D) g.create(); drawOperatorBackgrounds(process, gOB, printing); gOB.dispose(); // draw connections in own graphics context to avoid cross-phase manipulations Graphics2D gCO = (Graphics2D) g.create(); drawConnections(process, gCO, printing); gCO.dispose(); // draw operators in own graphics context to avoid cross-phase manipulations Graphics2D gOP = (Graphics2D) g.create(); drawOperators(process, gOP, printing); gOP.dispose(); // draw operator additions in own graphics context to avoid cross-phase manipulations Graphics2D gOA = (Graphics2D) g.create(); drawOperatorAdditions(process, gOA, printing); gOA.dispose(); // draw overlay in own graphics context to avoid cross-phase manipulations Graphics2D gOL = (Graphics2D) g.create(); drawOverlay(process, gOL, printing); gOL.dispose(); // let decorators draw Graphics2D gFG = (Graphics2D) g.create(); drawPhaseDecorators(process, gFG, RenderPhase.FOREGROUND, printing); gFG.dispose(); // restore non-scaled transform g.setTransform(at); // draw foreground in own graphics context to avoid cross-phase manipulations Graphics2D gFGF = (Graphics2D) g.create(); drawForeground(process, gFGF, printing); gFGF.dispose(); g.translate(model.getProcessWidth(process) + WALL_WIDTH * 2, 0); } g.dispose(); // draw wall element over execution engines Graphics2D gW = (Graphics2D) g2.create(); for (int i = 0; i < model.getProcesses().size() - 1; i++) { ExecutionUnit process = model.getProcesses().get(i); renderProcessWall(process, gW); gW.translate(model.getProcessWidth(process) + WALL_WIDTH * 2, 0); } // draw port connection attached to mouse over everything Port portSource = model.getConnectingPortSource(); if (portSource != null && model.getMousePositionRelativeToProcess() != null) { Graphics2D conG = (Graphics2D) g2.create(); // translate to correct position int width = 0; int index = model.getProcessIndex(portSource.getPorts().getOwner().getConnectionContext()); for (int i = 0; i < index; i++) { width += model.getProcessWidth(model.getProcess(i)) + WALL_WIDTH * 2; } conG.translate(width, 0); conG.scale(zoomFactor, zoomFactor); conG.setColor(ACTIVE_EDGE_COLOR); Point2D fromLoc = ProcessDrawUtils.createPortLocation(portSource, model); if (fromLoc != null) { double x = portSource instanceof OutputPort ? fromLoc.getX() + PORT_SIZE_HIGHLIGHT / 2 : fromLoc.getX() - PORT_SIZE_HIGHLIGHT / 2; conG.draw(new Line2D.Double(x, fromLoc.getY(), model.getMousePositionRelativeToProcess().getX(), model.getMousePositionRelativeToProcess().getY())); } conG.dispose(); } // draw selection rectangle over everything if (model.getSelectionRectangle() != null) { Graphics2D selG = (Graphics2D) g2.create(); selG.setPaint(SELECTION_RECT_PAINT); selG.setStroke(SELECTION_RECT_STROKE); selG.draw(model.getSelectionRectangle()); selG.dispose(); } } /** * Draws a wall (grey bar) right to the process box. Used for operator chains with multiple * units. * * @param process * the process to draw the wall for * @param g2 * the graphics context to draw on */ private void renderProcessWall(ExecutionUnit process, Graphics2D g2) { double width = model.getProcessWidth(process); double height = model.getProcessHeight(process); Shape wall = new Rectangle2D.Double(width, -10, 2 * WALL_WIDTH - 1, height + 20); g2.setColor(Colors.WINDOW_BACKGROUND); g2.fill(wall); g2.setColor(Colors.TEXTFIELD_BORDER); g2.draw(wall); } /** * Draws the process(es) background based on the {@link ProcessRendererModel} data and then * calls all registered {@link ProcessDrawDecorator}s for the background render phase. * * @param process * the process to draw the background for * @param g2 * the graphics context to draw upon * @param printing * if {@code true} we are printing instead of drawing to the screen */ public void drawBackground(final ExecutionUnit process, final Graphics2D g2, final boolean printing) { Graphics2D gBG = (Graphics2D) g2.create(); renderBackground(process, gBG, printing); gBG.dispose(); } /** * Draws process annotations and then calls all registered {@link ProcessDrawDecorator}s for the * annotations render phase. * * @param process * the process to draw the annotations for * @param g2 * the graphics context to draw upon * @param printing * if {@code true} we are printing instead of drawing to the screen */ public void drawAnnotations(final ExecutionUnit process, final Graphics2D g2, final boolean printing) { // let decorators draw drawPhaseDecorators(process, g2, RenderPhase.ANNOTATIONS, printing); } /** * Draws operator backgrounds and then calls all registered {@link ProcessDrawDecorator}s for * the annotations render phase. * * @param process * the process to draw the operator backgrounds for * @param g2 * the graphics context to draw upon * @param printing * if {@code true} we are printing instead of drawing to the screen */ public void drawOperatorBackgrounds(final ExecutionUnit process, final Graphics2D g2, final boolean printing) { Graphics2D gBG = (Graphics2D) g2.create(); // draw background of operators for (Operator op : process.getOperators()) { Rectangle2D frame = model.getOperatorRect(op); if (frame == null) { continue; } // only draw background if operator is visisble Rectangle2D opBounds = new Rectangle2D.Double(frame.getX() - 10, frame.getY(), frame.getWidth() + 20, frame.getHeight()); if (g2.getClipBounds() != null && !g2.getClipBounds().intersects(opBounds)) { continue; } renderOperatorBackground(op, gBG); } // draw connections background for all operators for (Operator operator : process.getOperators()) { renderConnectionsBackground(operator.getInputPorts(), operator.getOutputPorts(), gBG); } // draw connections background for process renderConnectionsBackground(process.getInnerSinks(), process.getInnerSources(), gBG); gBG.dispose(); // let decorators draw drawPhaseDecorators(process, g2, RenderPhase.OPERATOR_BACKGROUND, printing); } /** * Draws the process(es) operator connections based on the {@link ProcessRendererModel} data and * then calls all registered {@link ProcessDrawDecorator}s for the connections render phase. * * @param process * the process to draw the connections for * @param g2 * the graphics context to draw upon * @param printing * if {@code true} we are printing instead of drawing to the screen */ public void drawConnections(final ExecutionUnit process, final Graphics2D g2, final boolean printing) { Graphics2D gCo = (Graphics2D) g2.create(); // draw connections for all operators for (Operator operator : process.getOperators()) { renderConnections(operator.getOutputPorts(), gCo); } // draw connections for process renderConnections(process.getInnerSources(), gCo); gCo.dispose(); // let decorators draw drawPhaseDecorators(process, g2, RenderPhase.CONNECTIONS, printing); } /** * Draws operator annotations and then calls all registered {@link ProcessDrawDecorator}s for * the annotations render phase. * * @param process * the process to draw the operator annotations for * @param g2 * the graphics context to draw upon * @param printing * if {@code true} we are printing instead of drawing to the screen */ public void drawOperatorAnnotations(final ExecutionUnit process, final Graphics2D g2, final boolean printing) { // let decorators draw drawPhaseDecorators(process, g2, RenderPhase.OPERATOR_ANNOTATIONS, printing); } /** * Draws the process operators based on the {@link ProcessRendererModel} data including their * ports and the ports for the surrounding process and then calls all registered * {@link ProcessDrawDecorator}s for the operator render phase. * * @param process * the process to draw the operators for * @param g2 * the graphics context to draw upon * @param printing * if {@code true} we are printing instead of drawing to the screen */ public void drawOperators(final ExecutionUnit process, final Graphics2D g2, final boolean printing) { Graphics2D gOp = (Graphics2D) g2.create(); // draw not selected operators in order for (Operator op : process.getOperators()) { if (!model.getSelectedOperators().contains(op)) { drawOperator(op, true, gOp, printing); } } // draw selected operators in reverse order for correct z-levels List<Operator> drawList = new LinkedList<>(model.getSelectedOperators()); Collections.reverse(drawList); for (Operator op : drawList) { if (process.getOperators().contains(op)) { drawOperator(op, true, gOp, printing); } } // draw ports of the actual process renderPorts(process.getInnerSources(), gOp, true); renderPorts(process.getInnerSinks(), gOp, true); gOp.dispose(); // let decorators draw drawPhaseDecorators(process, g2, RenderPhase.OPERATORS, printing); } /** * Draws additions to operators and then calls all registered {@link ProcessDrawDecorator}s for * the operator additions render phase. * * @param process * the process to draw the operator additions for * @param g2 * the graphics context to draw upon * @param printing * if {@code true} we are printing instead of drawing to the screen */ public void drawOperatorAdditions(final ExecutionUnit process, final Graphics2D g2, final boolean printing) { // let decorators draw drawPhaseDecorators(process, g2, RenderPhase.OPERATOR_ADDITIONS, printing); } /** * Draws the process(es) operator overlay (i.e. execution order) based on the * {@link ProcessRendererModel} data and then calls all registered {@link ProcessDrawDecorator}s * for the overlay render phase. * * @param process * the process to draw the overlay for * @param g2 * the graphics context to draw upon * @param printing * if {@code true} we are printing instead of drawing to the screen */ public void drawOverlay(final ExecutionUnit process, final Graphics2D g2, final boolean printing) { // let decorators draw drawPhaseDecorators(process, g2, RenderPhase.OVERLAY, printing); } /** * Draws the process(es) foreground based on the {@link ProcessRendererModel} data and then * calls all registered {@link ProcessDrawDecorator}s for the foreground render phase. * * @param process * the process to draw the foreground for * @param g2 * the graphics context to draw upon * @param printing * if {@code true} we are printing instead of drawing to the screen */ public void drawForeground(final ExecutionUnit process, final Graphics2D g2, final boolean printing) { Graphics2D gBG = (Graphics2D) g2.create(); renderForeground(process, gBG, printing); gBG.dispose(); } /** * Draws the given {@link Operator} if inside the graphics clip bounds. * * @param op * the operator to draw. Note that it must have a position attached, see * {@link GUIProcessXMLFilter} * @param drawPorts * if {@true} will also draw operator ports, otherwise will not draw ports * @param g2 * the graphics context to draw upon * @param printing * if {@code true} we are printing instead of drawing to the screen * */ public void drawOperator(final Operator op, final boolean drawPorts, final Graphics2D g2, final boolean printing) { Rectangle2D frame = model.getOperatorRect(op); if (frame == null) { return; } // only draw operator if visible Rectangle2D opBounds = new Rectangle2D.Double(frame.getX() - 10, frame.getY(), frame.getWidth() + 20, frame.getHeight()); if (g2.getClipBounds() != null && !g2.getClipBounds().intersects(opBounds)) { return; } renderOperator(op, g2); renderPorts(op.getInputPorts(), g2, op.isEnabled()); renderPorts(op.getOutputPorts(), g2, op.isEnabled()); // let operator decorators draw drawOperatorDecorators(op, g2, printing); } /** * Adds the given draw decorator for the specified render phase. * * @param decorator * the decorator instance to add * @param phase * the phase during which the decorator should be called to draw. If multiple * decorators want to draw during the same phase, they are called in the order they * were registered */ public void addDecorator(final ProcessDrawDecorator decorator, final 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. * * @param decorator * the decorator instance to remove * @param phase * the phase from which the decorator should be removed */ public void removeDecorator(final ProcessDrawDecorator decorator, final 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); } /** * Adds the given operator draw decorator. The decorator is called directly after the operator * was drawn. * * @param decorator * the decorator instance to add */ public void addDecorator(final OperatorDrawDecorator decorator) { if (decorator == null) { throw new IllegalArgumentException("decorator must not be null!"); } operatorDecorators.add(decorator); } /** * Removes the given operator decorator. If the decorator has already been removed, does * nothing. * * @param decorator * the decorator instance to remove */ public void removeDecorator(final OperatorDrawDecorator decorator) { if (decorator == null) { throw new IllegalArgumentException("decorator must not be null!"); } operatorDecorators.remove(decorator); } /** * Draws the given {@link Operator}. * * @param operator * the operator to draw * @param g2 * the graphics context */ private void renderOperator(final Operator operator, final Graphics2D g2) { Rectangle2D frame = model.getOperatorRect(operator); // the first paint can come before any of the operator register listeners fire // thus we need to check the rect for null and set it here once // all subsequent calls will then have a valid rect if (frame == null) { return; } boolean isSelected = model.getSelectedOperators().contains(operator); boolean isHovered = operator == model.getHoveringOperator(); double headerWidth; Rectangle2D nameBounds = OPERATOR_FONT.getStringBounds(operator.getName(), g2.getFontRenderContext()); if (isSelected) { headerWidth = nameBounds.getWidth() + 6; } else { headerWidth = frame.getWidth() * MAX_HEADER_RATIO; } Shape bodyShape = new RoundRectangle2D.Double(frame.getMinX(), frame.getMinY() + ProcessRendererModel.HEADER_HEIGHT, frame.getWidth(), frame.getHeight() - ProcessRendererModel.HEADER_HEIGHT, OPERATOR_CORNER, OPERATOR_CORNER); // Frame Body Color baseColor = SwingTools.getOperatorColor(operator); if (!operator.isEnabled()) { baseColor = Color.LIGHT_GRAY; } g2.setPaint(baseColor); g2.fill(bodyShape); g2.setPaint(LINE_COLOR); g2.setStroke(LINE_STROKE); if (isSelected) { g2.setPaint(OPERATOR_BORDER_COLOR_SELECTED); g2.setStroke(OPERATOR_STROKE_SELECTED); } else if (isHovered) { g2.setPaint(OPERATOR_BORDER_COLOR_HIGHLIGHT); g2.setStroke(OPERATOR_STROKE_HIGHLIGHT); } else { g2.setPaint(OPERATOR_BORDER_COLOR); g2.setStroke(OPERATOR_STROKE_NORMAL); } g2.draw(bodyShape); // Label: Name g2.setFont(OPERATOR_FONT); String name = ProcessDrawUtils.fitString(operator.getName(), g2, (int) headerWidth); // take smallest width and center name double relevantWidth = nameBounds.getWidth() < headerWidth ? nameBounds.getWidth() : headerWidth; double offset = (frame.getWidth() - relevantWidth) / 2; int x = (int) (frame.getX() + offset); // draw white badge behind operator name if ((isHovered || isSelected) && nameBounds.getWidth() > frame.getWidth()) { g2.setColor(Color.WHITE); int padding = 5; g2.fillRoundRect((int) Math.min(frame.getX() - padding, x - padding), (int) frame.getY() - 3, (int) Math.max(frame.getWidth() + 2 * padding, relevantWidth + 2 * padding), ProcessRendererModel.HEADER_HEIGHT + 3, OPERATOR_BG_CORNER, OPERATOR_BG_CORNER); } if (operator.isEnabled()) { if (isSelected) { g2.setColor(OPERATOR_NAME_COLOR_SELECTED); } else if (isHovered) { g2.setColor(OPERATOR_NAME_COLOR_HIGHLIGHT); } else { g2.setColor(OPERATOR_NAME_COLOR); } } else { g2.setColor(OPERATOR_NAME_COLOR_DISABLED); } g2.drawString(name, x, (int) (frame.getY() + OPERATOR_FONT.getSize() + 1)); double yPosition = frame.getY() + ProcessRendererModel.HEADER_HEIGHT + 7; if (operator.isAnimating() && ProcessAnimationManager.INSTANCE.getAnimationForOperator(operator) != null) { // draw progress animation if operator is running AffineTransform transformBefore = g2.getTransform(); Animation animation = ProcessAnimationManager.INSTANCE.getAnimationForOperator(operator); Rectangle bounds = animation.getBounds(); g2.translate(frame.getX() + frame.getWidth() / 2, yPosition + OPERATOR_ICON_SIZE / 2); g2.scale(OPERATOR_ICON_SIZE / bounds.getWidth(), OPERATOR_ICON_SIZE / bounds.getHeight()); animation.draw(g2); g2.setTransform(transformBefore); } else { // Icon ImageIcon icon = operator.getOperatorDescription().getIcon(); if (icon != null) { if (!operator.isEnabled()) { icon = ProcessDrawUtils.getIcon(operator, icon); } icon.paintIcon(null, g2, (int) (frame.getX() + frame.getWidth() / 2 - icon.getIconWidth() / 2), (int) yPosition); } } // Small icons int iconX = (int) frame.getX() + 3; // Dirtyness ImageIcon opIcon; if (operator.isRunning()) { opIcon = OPERATOR_RUNNING; } else if (!operator.isDirty()) { opIcon = OPERATOR_READY; } else { opIcon = null; } if (opIcon != null) { ProcessDrawUtils.getIcon(operator, opIcon).paintIcon(null, g2, iconX, (int) (frame.getY() + frame.getHeight() - opIcon.getIconHeight() - 1)); } iconX += 16; // Errors if (!operator.getErrorList().isEmpty()) { int iconY = (int) (frame.getY() + frame.getHeight() - IMAGE_WARNING.getIconHeight() - 2); ProcessDrawUtils.getIcon(operator, IMAGE_WARNING).paintIcon(null, g2, iconX, iconY); } iconX += IMAGE_WARNING.getIconWidth() + 1; // Breakpoint if (operator.hasBreakpoint()) { ImageIcon breakpointIcon; if (operator.getNumberOfBreakpoints() == 1) { if (operator.hasBreakpoint(BreakpointListener.BREAKPOINT_BEFORE)) { breakpointIcon = IMAGE_BREAKPOINT_BEFORE; } else if (operator.hasBreakpoint(BreakpointListener.BREAKPOINT_AFTER)) { breakpointIcon = IMAGE_BREAKPOINT_AFTER; } else { breakpointIcon = IMAGE_BREAKPOINT_WITHIN; } } else { breakpointIcon = IMAGE_BREAKPOINTS; } ProcessDrawUtils.getIcon(operator, breakpointIcon).paintIcon(null, g2, iconX, (int) (frame.getY() + frame.getHeight() - breakpointIcon.getIconHeight() - 1)); } iconX += IMAGE_BREAKPOINTS.getIconWidth() + 1; // placeholder for workflow annotations icon iconX += IMAGE_BREAKPOINTS.getIconWidth() + 1; if (operator instanceof OperatorChain) { ProcessDrawUtils.getIcon(operator, IMAGE_BRANCH).paintIcon(null, g2, iconX, (int) (frame.getY() + frame.getHeight() - IMAGE_BRANCH.getIconHeight() - 1)); } iconX += IMAGE_BRANCH.getIconWidth() + 1; } /** * Draws the given {@link Ports}. * * @param ports * @param g * @param enabled */ private void renderPorts(final Ports<? extends Port> ports, final Graphics2D g, final boolean enabled) { boolean input = ports instanceof InputPorts; g.setStroke(LINE_STROKE); OutputPort hoveredConnectionSource = model.getHoveringConnectionSource(); OutputPort selectedConnectionSource = model.getSelectedConnectionSource(); Port connectingPort = model.getConnectingPortSource(); InputPort hoveredConnectionTarget = hoveredConnectionSource != null ? hoveredConnectionSource.getDestination() : null; InputPort selectedConnectionTarget = selectedConnectionSource != null ? selectedConnectionSource.getDestination() : null; Port hoveredPort = model.getHoveringPort(); Port hoveredPortConnectedTo = null; if (hoveredPort instanceof OutputPort) { hoveredPortConnectedTo = ((OutputPort) hoveredPort).getDestination(); } else if (hoveredPort instanceof InputPort) { hoveredPortConnectedTo = ((InputPort) hoveredPort).getSource(); } for (Port port : ports.getAllPorts()) { boolean hasError = !port.getErrors().isEmpty(); Point location = ProcessDrawUtils.createPortLocation(port, model); // the first paint can come before any of the operator register listeners fire // thus we need to check the location for null; subsequent calls will have a location if (location == null) { return; } // determine current state variables boolean isConHovered = port.equals(hoveredConnectionSource) || port.equals(hoveredConnectionTarget); boolean isConSelected = port.equals(selectedConnectionSource) || port.equals(selectedConnectionTarget); boolean isPortHovered = port.equals(hoveredPort) || port.equals(hoveredPortConnectedTo); boolean isConDirectlyHovered = isConHovered && !isPortHovered && hoveredPort == null && model.getHoveringOperator() == null && model.getConnectingPortSource() == null; boolean isConDirectlySelected = isConSelected && model.getConnectingPortSource() == null; Operator draggedOp = !model.getDraggedOperators().isEmpty() ? model.getDraggedOperators().get(0) : null; boolean isDragTarget = isConHovered && model.getDraggedOperators().size() == 1 && ProcessDrawUtils.canOperatorBeInsertedIntoConnection(model, draggedOp); boolean isConnectingSource = port.equals(connectingPort); boolean isProcessPort = port.getPorts().getOwner().getOperator() == model.getDisplayedChain(); double x = location.getX(); double y = location.getY(); Color line = isProcessPort ? BORDER_COLOR : OPERATOR_BORDER_COLOR; Color fill = Color.WHITE; Shape ellipseTop, ellipseBottom, ellipseBoth; int startAngle; int portSize; if (isConDirectlyHovered) { portSize = PORT_SIZE_HIGHLIGHT; } else if (isConDirectlySelected) { portSize = PORT_SIZE_HIGHLIGHT; } else if (isDragTarget) { portSize = PORT_SIZE_HIGHLIGHT; } else if (isPortHovered) { portSize = PORT_SIZE_HIGHLIGHT; } else if (isConnectingSource) { portSize = PORT_SIZE_HIGHLIGHT; } else { portSize = PORT_SIZE; } if (input) { startAngle = 90; ellipseTop = new Arc2D.Double(new Rectangle2D.Double(x - portSize / 2, y - portSize / 2, portSize, portSize), startAngle, 90, Arc2D.PIE); ellipseBottom = new Arc2D.Double( new Rectangle2D.Double(x - portSize / 2, y - portSize / 2, portSize, portSize), startAngle + 90, 90, Arc2D.PIE); ellipseBoth = new Arc2D.Double( new Rectangle2D.Double(x - portSize / 2, y - portSize / 2, portSize, portSize), startAngle, 180, Arc2D.PIE); } else { startAngle = 270; ellipseBottom = new Arc2D.Double( new Rectangle2D.Double(x - portSize / 2, y - portSize / 2, portSize, portSize), startAngle, 90, Arc2D.PIE); ellipseTop = new Arc2D.Double(new Rectangle2D.Double(x - portSize / 2, y - portSize / 2, portSize, portSize), startAngle + 90, 90, Arc2D.PIE); ellipseBoth = new Arc2D.Double( new Rectangle2D.Double(x - portSize / 2, y - portSize / 2, portSize, portSize), startAngle, 180, Arc2D.PIE); } if (enabled) { // What we have if (!hasError) { fill = ProcessDrawUtils.getColorFor(port, Color.WHITE, true); } else { fill = Color.RED; } g.setColor(fill); if (port instanceof OutputPort) { g.fill(ellipseBoth); } // What we want if (port instanceof InputPort) { g.fill(ellipseTop); InputPort inPort = (InputPort) port; for (Precondition precondition : inPort.getAllPreconditions()) { g.setColor(ProcessDrawUtils.getColorFor(precondition.getExpectedMetaData())); break; } g.fill(ellipseBottom); } g.setColor(line); if (model.getHoveringPort() == port) { g.setStroke(PORT_HIGHLIGHT_STROKE); } else { g.setStroke(PORT_STROKE); } g.draw(ellipseBoth); } else { g.setColor(fill); g.fill(ellipseBoth); g.setColor(line); if (model.getHoveringPort() == port) { g.setStroke(PORT_HIGHLIGHT_STROKE); } else { g.setStroke(PORT_STROKE); } g.draw(ellipseBoth); } g.setFont(PORT_FONT); int xt; int yt = 0; Rectangle2D strBounds = PORT_FONT.getStringBounds(port.getShortName(), g.getFontRenderContext()); if (isProcessPort) { if (input) { xt = -(int) (portSize / 2 + strBounds.getWidth() + 3); } else { xt = portSize - 3; } // connected process ports have their label above the connection if (port.isConnected() || isConnectingSource) { yt = 5; } } else { if (input) { xt = PORT_SIZE / 2; } else { xt = -(int) strBounds.getWidth() - 3; } } if (hasError) { g.setColor(Color.RED); g.setFont(PORT_FONT.deriveFont(Font.BOLD)); } else { if (port == model.getHoveringPort()) { g.setColor(PORT_NAME_SELECTION_COLOR); } else { g.setColor(PORT_NAME_COLOR); } } g.drawString(port.getShortName(), (int) x + xt, (int) (y - yt + strBounds.getHeight() / 2 - 2)); } } /** * Draws the connections for the given ports. * * @param ports * the output ports for which to draw connections * @param g2 * the graphics context to draw upon */ @SuppressWarnings("deprecation") private void renderConnections(final OutputPorts ports, final Graphics2D g2) { for (int i = 0; i < ports.getNumberOfPorts(); i++) { OutputPort from = ports.getPortByIndex(i); Port to = from.getDestination(); g2.setColor(ProcessDrawUtils.getColorFor(from, Color.LIGHT_GRAY, true)); if (to != null) { Shape connector = ProcessDrawUtils.createConnector(from, to, model); // the first paint can come before any of the operator register listeners fire // thus we need to check the shape for null; subsequent calls will have a valid // shape if (connector == null) { return; } // determine current state variables boolean isConHovered = from == model.getHoveringConnectionSource(); boolean isConSelected = from == model.getSelectedConnectionSource(); boolean isConDirectlyHovered = isConHovered && model.getHoveringPort() == null && model.getHoveringOperator() == null && model.getConnectingPortSource() == null; boolean isConDirectlySelected = isConSelected && model.getConnectingPortSource() == null; Operator draggedOp = !model.getDraggedOperators().isEmpty() ? model.getDraggedOperators().get(0) : null; boolean isDragTarget = isConHovered && model.getDraggedOperators().size() == 1 && ProcessDrawUtils.canOperatorBeInsertedIntoConnection(model, draggedOp); boolean portHovered = from == model.getHoveringPort() || to == model.getHoveringPort(); if (from.getMetaData() instanceof CollectionMetaData) { if (isDragTarget) { g2.setStroke(CONNECTION_COLLECTION_HIGHLIGHT_STROKE); } else if (isConDirectlyHovered) { g2.setStroke(CONNECTION_COLLECTION_HIGHLIGHT_STROKE); } else if (isConDirectlySelected) { g2.setStroke(CONNECTION_COLLECTION_HIGHLIGHT_STROKE); } else if (portHovered) { g2.setStroke(CONNECTION_COLLECTION_HIGHLIGHT_STROKE); } else { g2.setStroke(CONNECTION_COLLECTION_LINE_STROKE); } g2.draw(connector); g2.setColor(Color.white); g2.setStroke(LINE_STROKE); g2.draw(connector); } else { if (isDragTarget) { g2.setStroke(CONNECTION_HIGHLIGHT_STROKE); } else if (isConDirectlyHovered) { g2.setStroke(CONNECTION_HIGHLIGHT_STROKE); } else if (isConDirectlySelected) { g2.setStroke(CONNECTION_HIGHLIGHT_STROKE); } else if (portHovered) { g2.setStroke(CONNECTION_HIGHLIGHT_STROKE); } else { g2.setStroke(CONNECTION_LINE_STROKE); } g2.draw(connector); } } } } /** * Draws the operator background (white round rectangle). * * @param operator * the operator to draw the background for * @param g2 * the graphics context to draw upon */ private void renderOperatorBackground(final Operator operator, final Graphics2D g2) { Rectangle2D frame = model.getOperatorRect(operator); // the first paint can come before any of the operator register listeners fire // thus we need to check the rect for null and set it here once // all subsequent calls will then have a valid rect if (frame == null) { return; } RoundRectangle2D background = new RoundRectangle2D.Double(frame.getX() - 7, frame.getY() - 3, frame.getWidth() + 14, frame.getHeight() + 11, OPERATOR_BG_CORNER, OPERATOR_BG_CORNER); g2.setColor(Color.WHITE); g2.fill(background); // if name is wider than operator, extend white background for header Rectangle2D nameBounds = OPERATOR_FONT.getStringBounds(operator.getName(), g2.getFontRenderContext()); if (nameBounds.getWidth() > frame.getWidth()) { double relevantWidth = Math.min(nameBounds.getWidth(), frame.getWidth() * MAX_HEADER_RATIO); double offset = (frame.getWidth() - relevantWidth) / 2; int x = (int) (frame.getX() + offset); int padding = 5; RoundRectangle2D nameBackground = new RoundRectangle2D.Double( (int) Math.min(frame.getX() - padding, x - padding), frame.getY() - 3, relevantWidth + 2 * padding, ProcessRendererModel.HEADER_HEIGHT + 3, OPERATOR_BG_CORNER, OPERATOR_BG_CORNER); g2.fill(nameBackground); } // render ports renderPortsBackground(operator.getInputPorts(), g2); renderPortsBackground(operator.getOutputPorts(), g2); } /** * Draws the connections background (round pipe) for the given ports. * * @param inputPorts * the input ports for which to draw connection backgrounds * @param ports * the output ports for which to draw connection backgrounds * @param g2 * the graphics context to draw upon */ @SuppressWarnings("deprecation") private void renderConnectionsBackground(final InputPorts inputPorts, final OutputPorts ports, final Graphics2D g2) { for (int i = 0; i < ports.getNumberOfPorts(); i++) { OutputPort from = ports.getPortByIndex(i); Port to = from.getDestination(); if (to != null) { Shape connector = ProcessDrawUtils.createConnector(from, to, model); // the first paint can come before any of the operator register listeners fire // thus we need to check the shape for null; subsequent calls will have a valid // shape if (connector == null) { return; } g2.setColor(Color.WHITE); if (from.getMetaData() instanceof CollectionMetaData) { g2.setStroke(CONNECTION_COLLECTION_LINE_BACKGROUND_STROKE); } else { g2.setStroke(CONNECTION_LINE_BACKGROUND_STROKE); } g2.draw(connector); } } } /** * Draws the given {@link Ports}. * * @param ports * @param g2 */ private void renderPortsBackground(final Ports<? extends Port> ports, final Graphics2D g2) { boolean input = ports instanceof InputPorts; g2.setStroke(LINE_STROKE); for (Port port : ports.getAllPorts()) { Point location = ProcessDrawUtils.createPortLocation(port, model); // the first paint can come before any of the operator register listeners fire // thus we need to check the location for null; subsequent calls will have a location if (location == null) { return; } double x = location.getX(); double y = location.getY(); Shape ellipseBoth; int startAngle; if (input) { startAngle = 90; ellipseBoth = new Arc2D.Double(new Rectangle2D.Double(x - PORT_SIZE_BACKGROUND / 2, y - PORT_SIZE_BACKGROUND / 2, PORT_SIZE_BACKGROUND, PORT_SIZE_BACKGROUND), startAngle, 180, Arc2D.PIE); } else { startAngle = 270; ellipseBoth = new Arc2D.Double(new Rectangle2D.Double(x - PORT_SIZE_BACKGROUND / 2, y - PORT_SIZE_BACKGROUND / 2, PORT_SIZE_BACKGROUND, PORT_SIZE_BACKGROUND), startAngle, 180, Arc2D.PIE); } g2.setColor(Color.WHITE); g2.fill(ellipseBoth); } } /** * Draws the background for the given process. * * @param process * the process for which to render the background * @param g2 * the graphics context to draw upon */ private void renderBackground(final ExecutionUnit process, final Graphics2D g2, boolean printing) { double width = model.getProcessWidth(process); double height = model.getProcessHeight(process); Shape frame = new Rectangle2D.Double(0, 0, width, height); Color currentInnerColor = INNER_COLOR; // background color g2.setColor(currentInnerColor); g2.fill(frame); // remember pre-scaling transform AffineTransform at = g2.getTransform(); g2.scale(model.getZoomFactor(), model.getZoomFactor()); // process title g2.setColor(PROCESS_TITLE_COLOR); g2.setFont(PROCESS_FONT); Operator displayedChain = process.getEnclosingOperator(); if (model.getProcesses().size() == 1) { g2.drawString(displayedChain.getName(), PROCESS_TITLE_PADDING + 2, PROCESS_FONT.getSize() + PROCESS_TITLE_PADDING); } else { // multiple subprocesses have special names so show them instead of operator name g2.drawString(process.getName(), PROCESS_TITLE_PADDING + 2, PROCESS_FONT.getSize() + PROCESS_TITLE_PADDING); } // restore transform g2.setTransform(at); // breakpoint if (displayedChain.hasBreakpoint()) { ImageIcon breakpointIcon; if (displayedChain.getNumberOfBreakpoints() == 1) { if (displayedChain.hasBreakpoint(BreakpointListener.BREAKPOINT_BEFORE)) { breakpointIcon = IMAGE_BREAKPOINT_BEFORE_LARGE; } else if (displayedChain.hasBreakpoint(BreakpointListener.BREAKPOINT_AFTER)) { breakpointIcon = IMAGE_BREAKPOINT_AFTER_LARGE; } else { breakpointIcon = IMAGE_BREAKPOINT_WITHIN_LARGE; } } else { breakpointIcon = IMAGE_BREAKPOINTS_LARGE; } ProcessDrawUtils.getIcon(displayedChain, breakpointIcon).paintIcon(null, g2, (int) width - PROCESS_TITLE_PADDING - IMAGE_BREAKPOINTS_LARGE.getIconWidth(), PROCESS_TITLE_PADDING); } boolean dragIndicate = false; if (drawHighlight && (model.isDragStarted() || model.isDropTargetSet() && model.isImportDragged()) || model.isOperatorSourceHovered()) { switch (RapidMinerGUI.getDragHighlighteMode()) { case FULL: case BORDER: dragIndicate = true; break; case NONE: default: break; } } // drag border if (dragIndicate && !printing) { if (RapidMinerGUI.getDragHighlighteMode() == DragHighlightMode.FULL) { // do nothing if we are in a tutorial process MainFrame mainFrame = RapidMinerGUI.getMainFrame(); if (mainFrame != null) { Process currentProcess = mainFrame.getProcess(); if (currentProcess != null) { ProcessRootOperator rootOperator = currentProcess.getRootOperator(); if (rootOperator != null && rootOperator.getUserData(Tutorial.KEY_USER_DATA_FLAG) != null) { return; } } } Font dragFont; if (width >= 600) { dragFont = DRAG_FONT_LARGE; } else if (width >= 400) { dragFont = DRAG_FONT_MEDIUM; } else { dragFont = DRAG_FONT_SMALL; } int padding = dragFont.getSize() / 3; if (drawHighlight && (model.isDragStarted() || model.isDropTargetSet() && model.isImportDragged())) { // drag here text g2.setFont(dragFont); Rectangle2D bounds = g2.getFontMetrics().getStringBounds(DROP_HERE, g2); int x = (int) (width / 2 - bounds.getWidth() / 2); int y = (int) (height / 2 + bounds.getHeight() / 2); g2.setColor(DRAG_BG_COLOR); int rX = x - padding; int rY = (int) (y - bounds.getHeight() / 2 - padding); g2.fillRoundRect(rX, rY, (int) (bounds.getWidth() + padding * 2), (int) bounds.getHeight(), RapidLookAndFeel.CORNER_DEFAULT_RADIUS, RapidLookAndFeel.CORNER_DEFAULT_RADIUS); drawCenteredText(process, g2, dragFont, DROP_HERE, DRAG_FG_COLOR, 0); } else if (model.isOperatorSourceHovered()) { // drop here text g2.setFont(dragFont); Rectangle2D bounds = g2.getFontMetrics().getStringBounds(DRAG_HERE, g2); int x = (int) (width / 2 - bounds.getWidth() / 2); int y = (int) (height / 2 + bounds.getHeight() / 2); g2.setColor(DRAG_BG_COLOR); int rX = x - padding; int rY = (int) (y - bounds.getHeight() / 2 - padding); g2.fillRoundRect(rX, rY, (int) (bounds.getWidth() + padding * 2), (int) bounds.getHeight(), RapidLookAndFeel.CORNER_DEFAULT_RADIUS, RapidLookAndFeel.CORNER_DEFAULT_RADIUS); drawCenteredText(process, g2, dragFont, DRAG_HERE, DRAG_FG_COLOR, 0); } } } else if (process.getEnclosingOperator() instanceof ProcessRootOperator && process.getAllInnerOperators().isEmpty() && ((ProcessRootOperator) process.getEnclosingOperator()).getUserData(Tutorial.KEY_USER_DATA_FLAG) == null) { // empty process hint text // but only if we are not in a tutorial process as indicated by the flag g2.setColor(HINT_COLOR); Font hintFont; if (width >= 700) { hintFont = HINT_FONT_LARGE; } else if (width >= 500) { hintFont = HINT_FONT_MEDIUM; } else { hintFont = HINT_FONT_SMALL; } double offset = hintFont.getSize() * 1.5; drawCenteredText(process, g2, hintFont, HINT_EMPTY_PROCESS_1, HINT_COLOR, -offset); drawCenteredText(process, g2, hintFont, HINT_EMPTY_PROCESS_2, HINT_COLOR, 0); drawCenteredText(process, g2, hintFont, HINT_EMPTY_PROCESS_3, HINT_COLOR, offset); } } /** * Renders the drag border if needed. * * @param process * the process for which to render the background * @param g2 * the graphics context to draw upon */ private void renderForeground(final ExecutionUnit process, final Graphics2D g2, boolean printing) { if (drawHighlight && !printing && (model.isDragStarted() || model.isDropTargetSet() && model.isImportDragged()) || model.isOperatorSourceHovered()) { switch (RapidMinerGUI.getDragHighlighteMode()) { case FULL: case BORDER: drawDragBorder(process, g2); break; case NONE: default: break; } } } /** * Draws the drag border. * * @param process * the process for which to render the background * @param g2 * the graphics context to draw upon */ private void drawDragBorder(final ExecutionUnit process, final Graphics2D g2) { double width = model.getProcessWidth(process); double height = model.getProcessHeight(process); Shape dragFrame = new RoundRectangle2D.Double(DRAG_BORDER_PADDING, DRAG_BORDER_PADDING, width - 2 * DRAG_BORDER_PADDING, height - 2 * DRAG_BORDER_PADDING, DRAG_BORDER_CORNER, DRAG_BORDER_CORNER); g2.setColor(BORDER_DRAG_COLOR); g2.setStroke(BORDER_DRAG_STROKE); g2.draw(dragFrame); } /** * Lets the decorators draw for the specified {@link RenderPhase}. * * @param process * the process which should be decorated * @param g2 * the graphics context. Each decorator gets a new context which is disposed * afterwards * @param phase * the render phase which determines the decorators that are called * @param printing * if {@code true} we are printing instead of drawing to the screen */ private void drawPhaseDecorators(final ExecutionUnit process, final Graphics2D g2, final RenderPhase phase, final boolean printing) { double width = model.getProcessWidth(process) * (1 / model.getZoomFactor()); double height = model.getProcessHeight(process) * (1 / model.getZoomFactor()); int borderWidth = 2; Shape frame = new Rectangle2D.Double(0 + borderWidth, 0 + borderWidth, width - 2 * borderWidth, height - 2 * borderWidth); for (ProcessDrawDecorator decorater : decorators.get(phase)) { Graphics2D g2Deco = null; try { g2Deco = (Graphics2D) g2.create(); g2Deco.clip(frame); if (printing) { decorater.print(process, g2Deco, model); } else { decorater.draw(process, g2Deco, model); } } catch (RuntimeException e) { // catch everything here LogService.getRoot().log(Level.WARNING, "com.rapidminer.gui.flow.processrendering.draw.ProcessDrawer.decorator_error", e); } finally { g2Deco.dispose(); } } } /** * Lets the decorators draw for the specified {@link RenderPhase}. * * @param operator * the operator which should be decorated * @param g2 * the graphics context. Each decorator gets a new context which is disposed * afterwards * @param printing * if {@code true} we are printing instead of drawing to the screen */ private void drawOperatorDecorators(final Operator operator, final Graphics2D g2, final boolean printing) { double width = model.getProcessWidth(operator.getExecutionUnit()) * (1 / model.getZoomFactor()); double height = model.getProcessHeight(operator.getExecutionUnit()) * (1 / model.getZoomFactor()); int borderWidth = 2; Shape frame = new Rectangle2D.Double(0 + borderWidth, 0 + borderWidth, width - 2 * borderWidth, height - 2 * borderWidth); for (OperatorDrawDecorator decorater : operatorDecorators) { Graphics2D g2Deco = null; try { g2Deco = (Graphics2D) g2.create(); g2Deco.clip(frame); if (printing) { decorater.print(operator, g2Deco, model); } else { decorater.draw(operator, g2Deco, model); } } catch (RuntimeException e) { // catch everything here LogService.getRoot().log(Level.WARNING, "com.rapidminer.gui.flow.processrendering.draw.ProcessDrawer.operator_decorator_error", e); } finally { g2Deco.dispose(); } } } /** * Draws text centered in the process. * * @param process * the process in question * @param g2 * the graphics context * @param font * @param text * @param color * @param yOffset */ private void drawCenteredText(ExecutionUnit process, Graphics2D g2, Font font, String text, Color color, double yOffset) { double width = model.getProcessWidth(process); double height = model.getProcessHeight(process); Graphics2D g2d = (Graphics2D) g2.create(); g2d.setFont(font); Rectangle2D bounds = g2d.getFontMetrics().getStringBounds(text, g2d); int x = (int) (width / 2 - bounds.getWidth() / 2); int y = (int) (height / 2 + bounds.getHeight() / 2 + yOffset); g2d.setColor(color); g2d.drawString(text, x, y); g2d.dispose(); } }