/** * 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.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.rapidminer.operator.ports.Port; import com.rapidminer.tools.Observable; import com.rapidminer.tools.Observer; import com.vlsolutions.swing.docking.RelativeDockablePosition; /** * This class creates a speech bubble-shaped JDialog, which can be attached to a {@link Port}. See * {@link BubbleWindow} for more details. * <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 PortInfoBubble extends BubbleWindow { /** * Builder for {@link PortInfoBubble}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 PortBubbleBuilder extends BubbleWindowBuilder<PortInfoBubble, PortBubbleBuilder> { private Port attachTo; private boolean hideOnConnection; private boolean hideOnDisable; private boolean hideOnRun; private boolean ensureVisible; private boolean killOnPerspectiveChange; public PortBubbleBuilder(final Window owner, final Port 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 port is connected. Defaults to {@code false}. * * @param hideOnConnection * {@code true} if the bubble should be hidden upon connection; {@code false} * otherwise * @return the builder instance */ public PortBubbleBuilder setHideOnConnection(final boolean hideOnConnection) { this.hideOnConnection = hideOnConnection; return this; } /** * Sets whether to hide the bubble when the operator the port is attached to 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 PortBubbleBuilder 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 PortBubbleBuilder 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 PortBubbleBuilder 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 PortBubbleBuilder setKillOnPerspectiveChange(final boolean killOnPerspectiveChange) { this.killOnPerspectiveChange = killOnPerspectiveChange; return this; } @Override public PortInfoBubble build() { return new PortInfoBubble(owner, style, alignment, i18nKey, attachTo, componentsToAdd, hideOnConnection, hideOnDisable, hideOnRun, ensureVisible, moveable, showCloseButton, killOnPerspectiveChange, arguments); } @Override public PortBubbleBuilder getThis() { return this; } } private static final long serialVersionUID = 1L; private Port port; private OperatorChain portChain; private boolean hideOnConnection; private boolean hideOnDisable; private boolean hideOnRun; private final boolean killOnPerspectiveChange; private ProcessRendererView renderer = RapidMinerGUI.getMainFrame().getProcessPanel().getProcessRenderer(); private JViewport viewport = RapidMinerGUI.getMainFrame().getProcessPanel().getViewPort(); private ProcessRendererEventListener rendererModelListener; private ExtendedProcessEditor processEditor; private ProcessStateListener processStateListener; private ChangeListener viewPortListener; private Observer<Port> portObserver; /** * Creates a BubbleWindow which points to a {@link Port}. * * @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 port 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 hideOnConnection * if {@code true}, the bubble will be removed once the port is connected * @param hideOnDisable * if {@code true}, the bubble will be removed once the operator of the port 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 */ private PortInfoBubble(Window owner, BubbleStyle style, AlignedSide preferredAlignment, String i18nKey, Port toAttach, JComponent[] componentsToAdd, boolean hideOnConnection, 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.port = toAttach; this.portChain = toAttach.getPorts().getOwner().getPortHandler(); this.hideOnConnection = hideOnConnection; this.hideOnDisable = hideOnDisable; this.hideOnRun = hideOnRun; this.killOnPerspectiveChange = killOnPerspectiveChange; // if we need to ensure that the bubble is visible: if (ensureVisible) { OperatorChain portParent = portChain; while (portParent.getParent() != null) { portParent = portParent.getParent(); } OperatorChain displayedParent = renderer.getModel().getDisplayedChain(); while (displayedParent.getParent() != null) { displayedParent = displayedParent.getParent(); } // switch to correct subprocess (if the bubble is part of a subprocess!) // if it belongs to a different process, don't switch displayed process boolean belongsToSameRoot = portParent == null || portParent.equals(displayedParent); if (belongsToSameRoot && !renderer.getModel().getDisplayedChain().equals(portChain)) { renderer.getModel().setDisplayedChainAndFire(portChain); } // 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); } // keyboard accessibility ActionListener closeOnEscape = new ActionListener() { @Override public void actionPerformed(ActionEvent e) { PortInfoBubble.this.killBubble(true); } }; getRootPane().registerKeyboardAction(closeOnEscape, KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), JComponent.WHEN_IN_FOCUSED_WINDOW); // try to scroll to operator. If not possible, scroll to port ProcessPanel processPanel = RapidMinerGUI.getMainFrame().getProcessPanel(); if (!processPanel.scrollToOperator(port.getPorts().getOwner().getOperator())) { processPanel.scrollToViewPosition(getObjectLocation()); } super.paint(false); } @Override protected void registerSpecificListener() { viewPortListener = new ChangeListener() { @Override public void stateChanged(ChangeEvent e) { PortInfoBubble.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(portChain)) { killBubble(true); } break; case PROCESS_ZOOM_CHANGED: case PROCESS_SIZE_CHANGED: PortInfoBubble.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(port.getPorts().getOwner().getOperator())) { PortInfoBubble.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 (!port.getPorts().getOwner().getOperator().isEnabled()) { killBubble(true); } // check if operator was removed from process and kill bubble if (port.getPorts().getOwner().getOperator().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 } }; portObserver = new Observer<Port>() { @Override public void update(Observable<Port> observable, Port arg) { if (port.isConnected()) { killBubble(true); } } }; if (hideOnConnection) { port.addObserver(portObserver, false); } if (hideOnRun) { RapidMinerGUI.getMainFrame().getProcess().addProcessStateListener(processStateListener); } viewport.addChangeListener(viewPortListener); renderer.getModel().registerEventListener(rendererModelListener); RapidMinerGUI.getMainFrame().addExtendedProcessEditor(processEditor); } @Override protected void unregisterSpecificListeners() { if (hideOnConnection) { port.removeObserver(portObserver); } if (hideOnRun) { RapidMinerGUI.getMainFrame().getProcess().removeProcessStateListener(processStateListener); } renderer.getModel().removeEventListener(rendererModelListener); viewport.removeChangeListener(viewPortListener); RapidMinerGUI.getMainFrame().removeExtendedProcessEditor(processEditor); } @Override protected Point getObjectLocation() { if (!viewport.isShowing()) { return new Point(0, 0); } // get all necessary parameters if (!getDockable().getComponent().isShowing()) { return new Point(0, 0); } Point portLoc = ProcessDrawUtils.createPortLocation(port, renderer.getModel()); if (portLoc == null) { return new Point(0, 0); } portLoc.x = (int) (portLoc.x * renderer.getModel().getZoomFactor()); portLoc.y = (int) (portLoc.y * renderer.getModel().getZoomFactor()); portLoc = ProcessDrawUtils.convertToAbsoluteProcessPoint(portLoc, renderer.getModel().getProcessIndex(port.getPorts().getOwner().getConnectionContext()), renderer.getModel()); if (portLoc == null) { return new Point(0, 0); } portLoc.translate(-getObjectWidth() / 2, -getObjectHeight() / 2); // calculate actual on screen loc of the port loc and return it Point rendererLoc = renderer.getVisibleRect().getLocation(); Point absoluteLoc = new Point((int) (viewport.getLocationOnScreen().x + (portLoc.getX() - rendererLoc.getX())), (int) (viewport.getLocationOnScreen().y + (portLoc.getY() - rendererLoc.getY()))); // return validated Point return this.validatePointForBubbleInViewport(absoluteLoc); } @Override protected int getObjectWidth() { // double x width because we want a bit of distance from a port return (int) (ProcessDrawer.PORT_SIZE * 2 * renderer.getModel().getZoomFactor()); } @Override protected int getObjectHeight() { return (int) (ProcessDrawer.PORT_SIZE * 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); } }