/** * 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.tools.bubble; import java.awt.Point; import java.awt.Window; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.KeyEvent; import java.awt.geom.Rectangle2D; import java.util.Collection; import java.util.List; import javax.swing.JComponent; import javax.swing.JViewport; import javax.swing.KeyStroke; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import com.rapidminer.Process; import com.rapidminer.ProcessStateListener; import com.rapidminer.gui.PerspectiveModel; import com.rapidminer.gui.RapidMinerGUI; import com.rapidminer.gui.flow.ProcessPanel; import com.rapidminer.gui.flow.processrendering.annotations.model.WorkflowAnnotation; import com.rapidminer.gui.flow.processrendering.draw.ProcessDrawUtils; import com.rapidminer.gui.flow.processrendering.draw.ProcessDrawer; import com.rapidminer.gui.flow.processrendering.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.view.ProcessRendererView; import com.rapidminer.gui.processeditor.ExtendedProcessEditor; import com.rapidminer.gui.tools.DockingTools; import com.rapidminer.operator.Operator; import com.rapidminer.operator.OperatorChain; import com.vlsolutions.swing.docking.RelativeDockablePosition; /** * This class creates a speech bubble-shaped JDialog, which can be attached to an {@link Operator}. * See {@link BubbleWindow} for more details. In contrast to the {@link OperatorBubble}, this bubble * contains no assistants and behaves exactly like a {@link PortInfoBubble}. * <p> * If the perspective is incorrect, the dockable not shown or the subprocess currently viewed is * wrong, automatically corrects everything to ensure the bubble is shown if the * {@code ensureVisibility} parameter is set. * </p> * * @author Marco Boeck * @since 6.5.0 * */ public class OperatorInfoBubble extends BubbleWindow { /** * Builder for {@link OperatorInfoBubble}s. After calling all relevant setters, call * {@link #build()} to create the actual dialog instance. * * @author Marco Boeck * @since 6.5.0 * */ public static class OperatorBubbleBuilder extends BubbleWindowBuilder<OperatorInfoBubble, OperatorBubbleBuilder> { private Operator attachTo; private boolean hideOnDisable; private boolean hideOnRun; private boolean ensureVisible; private boolean killOnPerspectiveChange; public OperatorBubbleBuilder(final Window owner, final Operator attachTo, final String i18nKey, final Object... arguments) { super(owner, i18nKey, arguments); this.attachTo = attachTo; this.killOnPerspectiveChange = true; } /** * Sets whether to hide the bubble when the operator is disabled. Defaults to {@code false}. * * @param hideOnDisable * {@code true} if the bubble should be hidden upon disable; {@code false} * otherwise * @return the builder instance */ public OperatorBubbleBuilder setHideOnDisable(final boolean hideOnDisable) { this.hideOnDisable = hideOnDisable; return this; } /** * Sets whether to hide the bubble when the process is run. Defaults to {@code false}. * * @param hideOnRun * {@code true} if the bubble should be hidden upon running a process; * {@code false} otherwise * @return the builder instance */ public OperatorBubbleBuilder setHideOnProcessRun(final boolean hideOnRun) { this.hideOnRun = hideOnRun; return this; } /** * Sets whether to make sure the bubble is visible by automatically switching perspective, * opening/showing the process dockable and changing the subprocess. Defaults to * {@code false}. * * @param ensureVisible * {@code true} if the bubble should be hidden upon disable; {@code false} * otherwise * @return the builder instance */ public OperatorBubbleBuilder setEnsureVisible(final boolean ensureVisible) { this.ensureVisible = ensureVisible; return this; } /** * Sets whether the bubble should be automatically killed by switching perspective. Defaults * to {@code true}. * * @param killOnPerspectiveChange * {@code true} if the bubble should be killed on perspective change; * {@code false} otherwise * @return the builder instance */ public OperatorBubbleBuilder setKillOnPerspectiveChange(final boolean killOnPerspectiveChange) { this.killOnPerspectiveChange = killOnPerspectiveChange; return this; } @Override public OperatorInfoBubble build() { return new OperatorInfoBubble(owner, style, alignment, i18nKey, attachTo, componentsToAdd, hideOnDisable, hideOnRun, ensureVisible, moveable, showCloseButton, killOnPerspectiveChange, arguments); } @Override public OperatorBubbleBuilder getThis() { return this; } } private static final long serialVersionUID = 1L; private final Operator operator; private final OperatorChain operatorChain; private final boolean hideOnDisable; private final boolean hideOnRun; private final boolean killOnPerspectiveChange; private final ProcessRendererView renderer = RapidMinerGUI.getMainFrame().getProcessPanel().getProcessRenderer(); private final JViewport viewport = RapidMinerGUI.getMainFrame().getProcessPanel().getViewPort(); private ExtendedProcessEditor processEditor; private ProcessRendererEventListener rendererModelListener; private ProcessStateListener processStateListener; private ChangeListener viewPortListener; /** * Creates a BubbleWindow which points to an {@link Operator}. * * @param owner * the {@link Window} on which this {@link BubbleWindow} should be shown. * @param preferredAlignment * offer for alignment but the Class will calculate by itself whether the position is * usable. * @param i18nKey * of the message which should be shown * @param toAttach * the operator the bubble should be attached to * @param style * the bubble style * @param componentsToAdd * array of JComponents which will be added to the Bubble or {@code null} * @param hideOnDisable * if {@code true}, the bubble will be removed once the operator becomes disabled * @param hideOnRun * if {@code true}, the bubble will be removed once the process is executed * @param ensureVisible * if {@code true}, will automatically make sure the bubble will be visible by * manipulating the GUI * @param moveable * if {@code true} the user can drag the bubble around on screen * @param showCloseButton * if {@code true} the user can close the bubble via an "x" button in the top right * corner * @param killOnPerspectiveChange * if {@code true} the bubble will be automatically killed if the perspective changes * @param arguments * arguments to pass thought to the I18N Object */ OperatorInfoBubble(Window owner, BubbleStyle style, AlignedSide preferredAlignment, String i18nKey, Operator toAttach, JComponent[] componentsToAdd, boolean hideOnDisable, boolean hideOnRun, boolean ensureVisible, boolean moveable, boolean showCloseButton, boolean killOnPerspectiveChange, Object... arguments) { super(owner, style, preferredAlignment, i18nKey, ProcessPanel.PROCESS_PANEL_DOCK_KEY, null, null, moveable, showCloseButton, componentsToAdd, arguments); if (toAttach == null) { throw new IllegalArgumentException("toAttach must not be null!"); } this.operator = toAttach; if (operator.getParent() != null) { operatorChain = operator.getParent(); } else { this.operatorChain = operator instanceof OperatorChain ? (OperatorChain) operator : null; } this.hideOnDisable = hideOnDisable; this.hideOnRun = hideOnRun; this.killOnPerspectiveChange = killOnPerspectiveChange; // if we need to ensure that the bubble is visible: if (ensureVisible) { // switch to correct subprocess if (operatorChain != null && !renderer.getModel().getDisplayedChain().equals(operatorChain)) { renderer.getModel().setDisplayedChainAndFire(operatorChain); } // switch to correct perspective if (!RapidMinerGUI.getMainFrame().getPerspectiveController().getModel().getSelectedPerspective().getName() .equals(PerspectiveModel.DESIGN)) { RapidMinerGUI.getMainFrame().getPerspectiveController().showPerspective(PerspectiveModel.DESIGN); this.myPerspective = PerspectiveModel.DESIGN; } // make sure dockable is visible DockingTools.openDockable(ProcessPanel.PROCESS_PANEL_DOCK_KEY, null, RelativeDockablePosition.TOP_CENTER); // make sure the operator has a parent (which could be the ProcessRootOperator) // if the operator has no parent, e.g. because it is used internally by another // operator, it can not be selected if (operatorChain != null) { RapidMinerGUI.getMainFrame().selectOperator(operator); } } // keyboard accessibility ActionListener closeOnEscape = new ActionListener() { @Override public void actionPerformed(ActionEvent e) { OperatorInfoBubble.this.killBubble(true); } }; getRootPane().registerKeyboardAction(closeOnEscape, KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), JComponent.WHEN_IN_FOCUSED_WINDOW); super.paint(false); } @Override protected void registerSpecificListener() { viewPortListener = new ChangeListener() { @Override public void stateChanged(ChangeEvent e) { OperatorInfoBubble.this.paint(false); } }; rendererModelListener = new ProcessRendererEventListener() { @Override public void modelChanged(ProcessRendererModelEvent e) { switch (e.getEventType()) { case DISPLAYED_CHAIN_CHANGED: case DISPLAYED_PROCESSES_CHANGED: if (!renderer.getModel().getDisplayedChain().equals(operatorChain)) { killBubble(true); } break; case PROCESS_ZOOM_CHANGED: case PROCESS_SIZE_CHANGED: OperatorInfoBubble.this.paint(false); break; case MISC_CHANGED: case DISPLAYED_CHAIN_WILL_CHANGE: default: break; } } @Override public void operatorsChanged(ProcessRendererOperatorEvent e, Collection<Operator> operators) { switch (e.getEventType()) { case OPERATORS_MOVED: case PORTS_CHANGED: for (Operator op : operators) { if (op.equals(operator)) { OperatorInfoBubble.this.paint(false); break; } } break; case SELECTED_OPERATORS_CHANGED: default: break; } } @Override public void annotationsChanged(ProcessRendererAnnotationEvent e, Collection<WorkflowAnnotation> annotations) { // don't care } }; processEditor = new ExtendedProcessEditor() { @Override public void setSelection(List<Operator> selection) { // don't care } @Override public void processUpdated(Process process) { if (hideOnDisable) { // check if operator was disabled and kill bubble if that is desired if (!operator.isEnabled()) { killBubble(true); } // check if operator was removed from process and kill bubble if (operator.getExecutionUnit() == null) { killBubble(true); } } // don't care } @Override public void processChanged(Process process) { killBubble(true); } @Override public void processViewChanged(Process process) { // handled by model listener } }; processStateListener = new ProcessStateListener() { @Override public void started(Process process) { killBubble(true); } @Override public void paused(Process process) { // ignore } @Override public void resumed(Process process) { // ignore } @Override public void stopped(Process process) { // ignore } }; if (hideOnRun) { RapidMinerGUI.getMainFrame().getProcess().addProcessStateListener(processStateListener); } viewport.addChangeListener(viewPortListener); renderer.getModel().registerEventListener(rendererModelListener); RapidMinerGUI.getMainFrame().addExtendedProcessEditor(processEditor); } @Override protected void unregisterSpecificListeners() { renderer.getModel().removeEventListener(rendererModelListener); viewport.removeChangeListener(viewPortListener); RapidMinerGUI.getMainFrame().removeExtendedProcessEditor(processEditor); } @Override protected Point getObjectLocation() { // get all necessary parameters if (!getDockable().getComponent().isShowing()) { return new Point(0, 0); } Point rendererLoc = renderer.getVisibleRect().getLocation(); Rectangle2D targetRect = renderer.getModel().getOperatorRect(operator); if (targetRect == null) { return rendererLoc; } Point loc = new Point((int) targetRect.getX(), (int) targetRect.getY()); loc.x = (int) (loc.x * renderer.getModel().getZoomFactor()); loc.y = (int) (loc.y * renderer.getModel().getZoomFactor()); loc = ProcessDrawUtils.convertToAbsoluteProcessPoint(loc, renderer.getModel().getProcessIndex(operator.getExecutionUnit()), renderer.getModel()); if (loc == null) { return rendererLoc; } if (!viewport.isShowing()) { return new Point(0, 0); } // calculate actual on screen loc of the operator and return it Point absoluteLoc = new Point((int) (viewport.getLocationOnScreen().x + (loc.getX() - rendererLoc.getX())), (int) (viewport.getLocationOnScreen().y + (loc.getY() - rendererLoc.getY()))); // return validated Point return this.validatePointForBubbleInViewport(absoluteLoc); } @Override protected int getObjectWidth() { return (int) (ProcessDrawer.OPERATOR_WIDTH * renderer.getModel().getZoomFactor()); } @Override protected int getObjectHeight() { Rectangle2D rect = renderer.getModel().getOperatorRect(operator); int height = rect != null ? (int) rect.getHeight() : ProcessDrawer.OPERATOR_MIN_HEIGHT; return (int) (height * renderer.getModel().getZoomFactor()); } @Override protected void changeToAssistant(final AssistantType type) { if (AssistantType.WRONG_PERSPECTIVE == type && !killOnPerspectiveChange) { setVisible(false); } else { killBubble(true); } } /** * validates the position of a Bubble and manipulates the position so that the Bubble won't * point to a Point outside of the Viewport if the Operator is not in the Viewport (the * Alignment of the Bubble is considered). * * @param position * Point to validate * @return returns a Point inside the {@link JViewport} */ private Point validatePointForBubbleInViewport(Point position) { // calculate Offset which is necessary to consider the Alignment int xOffset = 0; int yOffset = 0; int x = position.x; int y = position.y; if (getRealAlignment() != null) { switch (getRealAlignment()) { case LEFTBOTTOM: case LEFTTOP: xOffset = this.getObjectWidth(); //$FALL-THROUGH$ case RIGHTBOTTOM: case RIGHTTOP: yOffset = (int) (this.getObjectHeight() * 0.5); break; case TOPLEFT: case TOPRIGHT: yOffset = this.getObjectHeight(); //$FALL-THROUGH$ case BOTTOMLEFT: case BOTTOMRIGHT: xOffset = (int) (this.getObjectWidth() * 0.5); break; // $CASES-OMITTED$ default: } } // manipulate invalid coordinates if (!(position.x + xOffset >= viewport.getLocationOnScreen().x)) { // left x = viewport.getLocationOnScreen().x - xOffset; } if (!(position.x + xOffset <= viewport.getLocationOnScreen().x + viewport.getSize().width)) { // right x = viewport.getLocationOnScreen().x + viewport.getSize().width - xOffset; } if (!(position.y + yOffset >= viewport.getLocationOnScreen().y)) { // top y = viewport.getLocationOnScreen().y - yOffset; } if (!(position.y + yOffset <= viewport.getLocationOnScreen().y + viewport.getSize().height)) { // bottom y = viewport.getLocationOnScreen().y + viewport.getSize().height - yOffset; } return new Point(x, y); } /** * @return the {@link Operator} for this bubble */ final Operator getOperator() { return operator; } }