/** * 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.BasicStroke; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Component; import java.awt.Container; import java.awt.Cursor; import java.awt.Dimension; import java.awt.Font; import java.awt.Frame; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.GraphicsDevice; import java.awt.GraphicsEnvironment; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.Insets; import java.awt.Point; import java.awt.RenderingHints; import java.awt.Shape; import java.awt.Window; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.ComponentEvent; import java.awt.event.ComponentListener; import java.awt.event.HierarchyEvent; import java.awt.event.HierarchyListener; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.awt.geom.AffineTransform; import java.awt.geom.GeneralPath; import java.util.LinkedList; import java.util.List; import java.util.logging.Level; import javax.swing.AbstractButton; import javax.swing.ImageIcon; import javax.swing.JButton; import javax.swing.JComponent; import javax.swing.JDialog; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.SwingUtilities; import javax.swing.event.MouseInputAdapter; import com.rapidminer.gui.Perspective; import com.rapidminer.gui.PerspectiveChangeListener; import com.rapidminer.gui.RapidMinerGUI; import com.rapidminer.gui.tools.ResourceAction; import com.rapidminer.gui.tools.SwingTools; import com.rapidminer.tools.I18N; import com.rapidminer.tools.LogService; import com.vlsolutions.swing.docking.Dockable; import com.vlsolutions.swing.docking.DockableState; import com.vlsolutions.swing.docking.DockingDesktop; import com.vlsolutions.swing.docking.event.DockableSelectionEvent; import com.vlsolutions.swing.docking.event.DockableSelectionListener; import com.vlsolutions.swing.docking.event.DockableStateChangeEvent; import com.vlsolutions.swing.docking.event.DockableStateChangeListener; import com.vlsolutions.swing.docking.event.DockingActionEvent; import com.vlsolutions.swing.docking.event.DockingActionListener; /** * This class creates a speech bubble-shaped JDialog, which can be attached to Buttons, Dockables or * Operators, either by using its ID. The bubble triggers two events which are obserable by the * {@link BubbleListener}; either if the close button was clicked, or if the corresponding button * was used. * <p> * To create instances, subclasses are encouraged to extend {@link BubbleWindowBuilder} to handle * bubble creation without exposing massive constructors or long constructor chains. * </p> * * @author Philipp Kersting and Thilo Kamradt */ public abstract class BubbleWindow extends JDialog { /** * Abstract builder for {@link BubbleWindow} implementations. After calling all relevant * setters, call {@link #build()} to create the actual bubble instance. * * @author Marco Boeck * @since 6.5.0 * */ protected static abstract class BubbleWindowBuilder<T extends BubbleWindow, U extends BubbleWindowBuilder<T, U>> { /** the owner window of the bubble, used for z-ordering by Swing. Can be {@code null} */ protected Window owner; /** * tthe i18n key for the bubble. Format: {@code gui.bubble.[i18nkey].title} and * {@code gui.bubble.[i18nkey].body} */ protected String i18nKey; /** optional i18n arguments */ protected Object[] arguments; /** the dockable to target of the bubble resides in. Can be {@code null} */ protected String dockKey; /** the style of the bubble, never {@code null} */ protected BubbleStyle style; /** the preferred relative position next to the target, never {@code null} */ protected AlignedSide alignment; /** the font for the title, can be {@code null} */ protected Font titleFont; /** the font for the body and all additional components, can be {@code null} */ protected Font bodyFont; /** whether the bubble window can be dragged around by the user */ protected boolean moveable; /** whether the bubble should have a close button in the top right corner */ protected boolean showCloseButton; /** * additional components which are added to the bottom of the bubble in a horizontal row. * Can be {@code null} */ protected JComponent[] componentsToAdd; /** * Creates a new builder for {@link BubbleWindow} implementations. Extend and overwrite * {@link #build()} for specific implementations. * * @param owner * the parent window for the bubble * @param i18nKey * the i18n key for the bubble. Format: {@code gui.bubble.[i18nkey].title} and * {@code gui.bubble.[i18nkey].body}. * @param arguments * optional i18n arguments */ public BubbleWindowBuilder(final Window owner, final String i18nKey, final Object... arguments) { if (i18nKey == null) { throw new IllegalArgumentException("i18nKey must not be null!"); } this.owner = owner; this.i18nKey = i18nKey; this.arguments = arguments; // default values this.style = BubbleStyle.COMIC; this.alignment = AlignedSide.BOTTOM; this.moveable = true; this.showCloseButton = true; } /** * Set the style of the bubble. See {@link BubbleStyle} for more information. Default is * {@link BubbleStyle#COMIC}. * * @param style * the style the bubble should have * @return the builder instance */ public U setStyle(final BubbleStyle style) { if (style == null) { throw new IllegalArgumentException("style must not be null!"); } this.style = style; return getThis(); } /** * Sets the preferred side where the bubble should be positioned relative to the target. If * the defined position is not possible, it will be changed automatically. Defaults to * {@link AlignedSide#BOTTOM}. * * @param alignment * the side relative to the target where the bubble should appear * @return the builder instance */ public U setAlignment(final AlignedSide alignment) { if (alignment == null) { throw new IllegalArgumentException("alignment must not be null!"); } this.alignment = alignment; return getThis(); } /** * Sets whether the bubble can be moved by dragging the title. Defaults to {@code true}. * * @param moveable * if {@code true} the user can drag the bubble around via the title * @return the builder instance */ public U setMoveable(final boolean moveable) { this.moveable = moveable; return getThis(); } /** * Sets the key of the {@link Dockable} the target component is in. This is useful if the * dockable is moved/removed by the user so the bubble can react automatically. * * @param dockKey * the key of the dockable * @return the builder instance */ public U setDockableOfTargetComponent(final String dockKey) { if (dockKey == null) { throw new IllegalArgumentException("dockKey must not be null!"); } this.dockKey = dockKey; return getThis(); } /** * Sets the font for the title text of the bubble. If not set, defaults to a font matching * the specified {@link BubbleStyle}. * * @param font * the font which should be used for the title * @return the builder instance */ public U setTitleFont(final Font font) { if (font == null) { throw new IllegalArgumentException("font must not be null!"); } this.titleFont = font; return getThis(); } /** * Sets the font for the body text of the bubble. If not set, defaults to a font matching * the specified {@link BubbleStyle}. * * @param font * the font which should be used for the body * @return the builder instance */ public U setBodyFont(final Font font) { if (font == null) { throw new IllegalArgumentException("font must not be null!"); } this.bodyFont = font; return getThis(); } /** * Sets additional components which are added to the bottom of the bubble in a horizontal * row. By default, nothing is visible below the body text and the bubble can only be closed * via the 'x' button in the top right corner. * * @param componentsToAdd * the components to add. Usually {@link JButton}s or similar * @return the builder instance */ public U setAdditionalComponents(final JComponent[] componentsToAdd) { if (componentsToAdd == null) { throw new IllegalArgumentException("componentsToAdd must not be null!"); } this.componentsToAdd = componentsToAdd; return getThis(); } /** * Hides the close button in the top right corner. Note that if it is hidden and no * additional components have been added, there is no way to close the bubble. * * @return the builder instance */ public U hideCloseButton() { this.showCloseButton = false; return getThis(); } /** * Creates the {@link BubbleWindow} implementation instance according to the specified * settings. * * @return the bubble instance, never {@code null} */ public abstract T build(); /** * Returns the implementation instance of the builder. Needed for chaining of setters in * abstract class. * * @return the actual builder implementation, never {@code null} */ public abstract U getThis(); } /** * The possible styles of a {@link BubbleWindow}. * * @since 6.5.0 * */ public static enum BubbleStyle { /** a bubble with a green style indicating everything is fine */ OK(FONT_TITLE, FONT_BODY, ICON_OK, new Color(173, 237, 200)), /** a bubble with a blue style indicating something if interest */ INFORMATION(FONT_TITLE, FONT_BODY, ICON_INFORMATION, new Color(177, 215, 241)), /** a bubble with a yellow style indicating a warning */ WARNING(FONT_TITLE, FONT_BODY, ICON_WARNING, new Color(250, 217, 164)), /** a bubble with a red style indicating a severe problem */ ERROR(FONT_TITLE, FONT_BODY, ICON_ERROR, new Color(245, 187, 181)), /** a bubble with a comic style which is used by the comic tutorials */ COMIC(FONT_TITLE_COMIC, FONT_BODY_COMIC, null, COLOR_BACKGROUND_COMIC); private final Font titleFont; private final Font bodyFont; private final ImageIcon icon; private final Color color; private BubbleStyle(final Font titleFont, final Font bodyFont, final ImageIcon icon, final Color color) { this.titleFont = titleFont; this.bodyFont = bodyFont; this.icon = icon; this.color = color; } /** * @return the title font, never {@code null} */ public Font getTitleFont() { return titleFont; } /** * @return the title font, never {@code null} */ public Font getBodyFont() { return bodyFont; } /** * @return the icon for this style or {@code null} if it has none */ public ImageIcon getIcon() { return icon; } /** * @return the bubble color, never {@code null} */ public Color getColor() { return color; } } /** * Indicates on which side of the target component the Bubble will be positioned. * */ public static enum AlignedSide { /** * to the right of the component */ RIGHT, /** * to the left of the component */ LEFT, /** * above the component */ TOP, /** * below the component */ BOTTOM, /** * in the middle of the component */ MIDDLE } /** * The listener which can be registered to be notified of buble events. See * {@link BubbleWindow#addBubbleListener(BubbleListener)}. * */ public static interface BubbleListener { /** * Called when the bubble has been closed. * * @param bw * the origin of the event */ public void bubbleClosed(BubbleWindow bw); /** * Called when {@link BubbleWindow#triggerFire()} is called. * * @param bw * the origin of the event */ public void actionPerformed(BubbleWindow bw); } /** * Used to define the position of the pointer of the bubble, aka the corner which points to the * component. */ protected enum Alignment { TOPLEFT, TOPRIGHT, BOTTOMLEFT, BOTTOMRIGHT, LEFTTOP, LEFTBOTTOM, RIGHTTOP, RIGHTBOTTOM, INNERRIGHT, INNERLEFT, /** * bubble is placed inside the component, no pointer. */ MIDDLE; } /** * Used to determine the type of a possible assistant bubble. */ protected enum AssistantType { /** * the dockable to which the bubble should be attached is not in the selected tab in a * multi-tab environment */ NOT_SHOWING, /** * the dockable to which the bubble should be attached is not on the screen */ NOT_ON_SCREEN, /** * user looks at wrong operator chain (subprocess) */ NOT_IN_CHAIN, /** * no assistent currently active */ NO_ASSISTANT_ACTIVE, /** * when the dockable of the target becomes hidden */ HIDDEN, /** * when the user is in the wrong perspective */ WRONG_PERSPECTIVE, } /** * Responsible for allowing to drag the bubble around by the user. * */ private class MoveListener extends MouseInputAdapter { private Component comp; private Point startLoc; private Point lastLoc; private MoveListener(Component comp) { this.comp = comp; } @Override public void mousePressed(MouseEvent e) { startLoc = e.getLocationOnScreen(); lastLoc = comp.getLocation(); } @Override public void mouseDragged(MouseEvent e) { int x = startLoc.x; int y = startLoc.y; int xOffset = e.getXOnScreen() - x; int yOffset = e.getYOnScreen() - y; Point newLoc = new Point(lastLoc.x + xOffset, lastLoc.y + yOffset); comp.setLocation(newLoc); } } private static final long serialVersionUID = -7508372660983304065L; private static final Font FONT_TITLE = new Font("Open Sans Light", Font.PLAIN, 18); private static final Font FONT_BODY = new Font("Open Sans", Font.PLAIN, 13); private static final Font FONT_TITLE_COMIC = new Font("AlterEgoBB", Font.PLAIN, 14).deriveFont(Font.BOLD); private static final Font FONT_BODY_COMIC = new Font("AlterEgoBB", Font.PLAIN, 13); private static final Color COLOR_BACKGROUND_COMIC = new Color(249, 200, 127); private static final Color COLOR_TRANSPARENT = new Color(0, 0, 0, 0); private static final float BORDER_STROKE_WIDTH = 1.5f; private static final ImageIcon ICON_INFORMATION = SwingTools .createIcon("flat_icons/white/192/" + I18N.getGUIMessage("gui.bubble.information.icon")); private static final ImageIcon ICON_WARNING = SwingTools .createIcon("flat_icons/white/192/" + I18N.getGUIMessage("gui.bubble.warning.icon")); private static final ImageIcon ICON_ERROR = SwingTools .createIcon("flat_icons/white/192/" + I18N.getGUIMessage("gui.bubble.error.icon")); private static final ImageIcon ICON_OK = SwingTools .createIcon("flat_icons/white/192/" + I18N.getGUIMessage("gui.bubble.ok.icon")); private static final String KEY_NOT_ON_SCREEN = "lostDockable"; private static final String KEY_NOT_SHOWING = "not_showing"; private static final String KEY_HIDDEN = "hiddenDockable"; private static final String KEY_PERSPECTIVE = "changePerspective"; /** * Width of the little pointer triangle attached to the bubble. check this value in case of a * redesign. */ private static final int BUBBLE_CONNECTOR_HEIGHT = 35; /** * Height of the little pointer triangle attached to the bubble. check this value in case of a * redesign. */ private static final int BUBBLE_CONNECTOR_WIDTH = 46; /** Radius of the rounded rectangle. */ private static final int CORNER_RADIUS = 20; /** Fixed width of the bubble. */ private static final int WINDOW_WIDTH = 225; /** Fixed width of the bubble for comics. */ private static final int WINDOW_WIDTH_COMIC = 200; /** high quality rendering hints */ private static final RenderingHints HI_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); HI_QUALITY_HINTS.put(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY); } protected static final int HIDDEN_WIDTH = 19; protected static final int HIDDEN_HEIGHT = 200; protected static final Point HIDDEN_POS = new Point(1, 24); /* * constants to return from the static methods isDockableOnScreen(String) and * isButtonOnSreen(String) */ public static final int OBJECT_SHOWING_ON_SCREEN = 0; public static final int OBJECT_NOT_SHOWING = 1; public static final int OBJECT_NOT_ON_SCREEN = -1; private final String i18nKey; private final Object[] arguments; private final Font titleFont; private final Font bodyFont; private final BubbleStyle style; private final AlignedSide preferredAlignment; private final List<BubbleListener> listeners = new LinkedList<>(); private final DockingDesktop desktop = RapidMinerGUI.getMainFrame().getDockingDesktop(); private boolean built = false; private Alignment realAlignment = Alignment.TOPLEFT; private GridBagConstraints constraints = null; private JPanel bubble; private JButton close; private ActionListener listener; private JLabel headline; private JLabel mainText; private String docKey = null; private Dockable dockable; private JComponent[] componentsInBubble; private boolean addPerspective = true; /** indicates whether the listeners have been added or not */ private boolean listenersAdded = false; private boolean isPerPixelTranslucencySupported; private boolean moveable; private boolean showCloseButton; private PerspectiveChangeListener perspectiveListener; private WindowAdapter windowListener; private ComponentListener compListener; private DockingActionListener dockListener; private HierarchyListener hierachyListener; private ComponentListener componentListenerToWindow; /* assistant Attributes */ private HierarchyListener assistantHierarchy; private BubbleWindow assistantBubble; private DockableStateChangeListener assistantDockStateChange; private DockableSelectionListener assistantDockSelect; private DockingActionListener assistantDockingAction; private PerspectiveChangeListener assistantPerspective; private AssistantType currentAssistant = AssistantType.NO_ASSISTANT_ACTIVE; private volatile boolean killed = false; private volatile int dockingCounter = 0; /** * the origin perspective where this bubble lives. If initial construction takes place in a * different perspective than where the bubble should live, change this */ protected String myPerspective; /** * creates a BubbleWindow-Object. To paint and repaint this BubbleWindow call paint(boolean * refreshListerns) * * @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 * key of the message which should be shown. * @param docKey * key of the Dockable the BubbleWindow will attach to. * @param componentsToAdd * Array of {@link JComponent}s which will be added to the Bubble (null instead of * the array won't throw an error). * @param arguments * arguments to pass thought to the I18N Object */ public BubbleWindow(final Window owner, final AlignedSide preferredAlignment, final String i18nKey, final String docKey, final JComponent[] componentsToAdd, final Object... arguments) { this(owner, BubbleStyle.COMIC, preferredAlignment, i18nKey, docKey, null, null, false, true, componentsToAdd, arguments); } /** * Creates an instance with the given parameters. Should be called with the arguments gathered * from {@link BubbleWindowBuilder} implementations. * * @param owner * the {@link Window} on which this {@link BubbleWindow} should be shown * @param style * the style the bubble should have * @param preferredAlignment * offer for alignment but the Class will calculate by itself whether the position is * usable * @param i18nKey * key of the message which should be shown * @param docKey * key of the Dockable the BubbleWindow will attach to * @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 titleFont * the font for the title, can be {@code null} * @param bodyFont * the font for the body, can be {@code null} * @param componentsToAdd * Array of {@link JComponent}s which will be added to the Bubble (null instead of * the array won't throw an error) * @param arguments * arguments to pass thought to the I18N Object */ protected BubbleWindow(final Window owner, final BubbleStyle style, final AlignedSide preferredAlignment, final String i18nKey, final String docKey, final Font titleFont, final Font bodyFont, final boolean moveable, final boolean showCloseButton, final JComponent[] componentsToAdd, final Object... arguments) { super(owner); // if this check fails, the bubbles will look very ugly, but we can't change that I guess GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment(); GraphicsDevice gd = ge.getDefaultScreenDevice(); isPerPixelTranslucencySupported = gd .isWindowTranslucencySupported(GraphicsDevice.WindowTranslucency.PERPIXEL_TRANSLUCENT); if (!isPerPixelTranslucencySupported) { LogService.getRoot().log(Level.WARNING, "com.rapidminer.gui.tools.bubble.BubbleWindow.per_pixel_not_supported"); } this.i18nKey = i18nKey; this.arguments = arguments; this.myPerspective = getCurrentPerspectiveName(); this.preferredAlignment = preferredAlignment; this.moveable = moveable; if (docKey != null) { this.docKey = docKey; dockable = desktop.getContext().getDockableByKey(docKey); } this.style = style; if (titleFont != null) { this.titleFont = titleFont; } else { this.titleFont = style.getTitleFont(); } if (bodyFont != null) { this.bodyFont = bodyFont; } else { this.bodyFont = style.getBodyFont(); } this.showCloseButton = showCloseButton; if (componentsToAdd == null) { componentsInBubble = new JComponent[] {}; } else { componentsInBubble = componentsToAdd; } } /** * should be used to update the Bubble. Call this instead of repaint and similar. Update the * Alignment, shape and location. Also this method builds the Bubble by the first call. * * @param reregisterListerns * indicates whether the the Listeners will be refreshed too or not */ public void paint(final boolean refreshListerns) { if (!built) { this.buildBubble(); } this.paintAgain(refreshListerns); } /** * builds the Bubble for the first time */ private void buildBubble() { built = true; this.realAlignment = this.calculateAlignment(this.realAlignment); setLayout(new BorderLayout()); setFocusable(false); setFocusableWindowState(false); setUndecorated(true); if (isPerPixelTranslucencySupported) { setBackground(COLOR_TRANSPARENT); } else { setBackground(Color.WHITE); } initRegularListener(); GridBagLayout gbl = new GridBagLayout(); bubble = new JPanel(gbl) { private static final long serialVersionUID = 1L; @Override public void paintComponent(final Graphics g) { super.paintComponent(g); Graphics2D g2 = (Graphics2D) g.create(); g2.setRenderingHints(HI_QUALITY_HINTS); Shape shape = createShape(realAlignment, BORDER_STROKE_WIDTH); if (style == BubbleStyle.COMIC) { g2.setColor(COLOR_BACKGROUND_COMIC); } else { g2.setColor(style.getColor()); } g2.fill(shape); // draw icon in bottom right corner if existing ImageIcon icon = style.getIcon(); if (icon != null) { int xRight = (int) shape.getBounds().getMaxX(); int yBottom = (int) shape.getBounds().getMaxY(); if (realAlignment == Alignment.BOTTOMLEFT || realAlignment == Alignment.BOTTOMRIGHT) { yBottom -= CORNER_RADIUS; } if (realAlignment == Alignment.RIGHTBOTTOM || realAlignment == Alignment.RIGHTTOP || realAlignment == Alignment.INNERRIGHT) { xRight -= CORNER_RADIUS; } int iconW = icon.getIconWidth(); int iconH = icon.getIconHeight(); int x = (int) (xRight - iconW * 0.80f); int y = (int) (yBottom - iconH * 0.80f); // prevent icon drawing over border via clip Graphics2D gImg = (Graphics2D) g2.create(); Shape previousClip = gImg.getClip(); Shape iconClipShape = createShape(realAlignment, BORDER_STROKE_WIDTH); gImg.setClip(iconClipShape); gImg.drawImage(icon.getImage(), x, y, this); gImg.setClip(previousClip); gImg.dispose(); } g2.setColor(Color.BLACK); g2.setStroke(new BasicStroke(BORDER_STROKE_WIDTH, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER)); g2.draw(shape); g2.dispose(); } }; bubble.setSize(getSize()); bubble.setOpaque(false); bubble.setDoubleBuffered(false); add(bubble, BorderLayout.CENTER); // headline label headline = new JLabel(I18N.getMessage(I18N.getGUIBundle(), "gui.bubble." + i18nKey + ".title")); headline.setFont(titleFont); int width = style != BubbleStyle.COMIC ? WINDOW_WIDTH : WINDOW_WIDTH_COMIC; headline.setMinimumSize(new Dimension(width, 25)); headline.setPreferredSize(new Dimension(width, 25)); if (moveable) { MoveListener listener = new MoveListener(this); headline.addMouseListener(listener); headline.addMouseMotionListener(listener); headline.setCursor(Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR)); } // mainText label mainText = new JLabel("<html><div style=\"line-height: 150%;width:" + width + "px \">" + I18N.getMessage(I18N.getGUIBundle(), "gui.bubble." + i18nKey + ".body", arguments) + "</div></html>"); mainText.setFont(this.bodyFont); mainText.setMinimumSize(new Dimension(150, 20)); mainText.setMaximumSize(new Dimension(width, 800)); // create and add close Button for the Bubble if needed close = new JButton("x"); close.setFont(titleFont.deriveFont(Font.BOLD, titleFont.getSize())); close.setBorderPainted(true); close.setContentAreaFilled(false); final Color hoveredColor = new Color(110, 110, 110); final Color notHoveredColor = close.getForeground(); // change Icons and set close operation close.addActionListener(new ActionListener() { @Override public void actionPerformed(final ActionEvent e) { BubbleWindow.this.dispose(); fireEventCloseClicked(); } }); close.addMouseListener(new MouseAdapter() { @Override public void mouseExited(final MouseEvent e) { close.setForeground(notHoveredColor); } @Override public void mouseEntered(final MouseEvent e) { close.setForeground(hoveredColor); } }); close.setMargin(new Insets(0, 5, 0, 5)); // modify components for (int i = 0; i < this.componentsInBubble.length; i++) { this.componentsInBubble[i].setFont(bodyFont); this.componentsInBubble[i].setOpaque(false); this.componentsInBubble[i].setFont(bodyFont); } layoutBubble(); pack(); if (this.calculateAlignment(this.realAlignment) == this.realAlignment) { positionRelative(); } } /** * Adds the components to the bubble. */ private void layoutBubble() { bubble.removeAll(); constraints = new GridBagConstraints(); Insets insetsLabel = new Insets(10, 10, 10, 10); Insets insetsMainText = new Insets(0, 10, 10, 10); int numberAdditionalComponents = this.componentsInBubble.length; switch (realAlignment) { case TOPLEFT: insetsLabel = new Insets(CORNER_RADIUS + 15, 10, 10, 10); break; case TOPRIGHT: insetsLabel = new Insets(CORNER_RADIUS + 15, 10, 10, 10); break; case INNERLEFT: case LEFTTOP: insetsLabel = new Insets(10, CORNER_RADIUS + 15, 10, 10); insetsMainText = new Insets(0, CORNER_RADIUS + 15, 10, 10); break; case LEFTBOTTOM: insetsLabel = new Insets(10, CORNER_RADIUS + 15, 10, 10); insetsMainText = new Insets(0, CORNER_RADIUS + 15, 10, 10); break; case BOTTOMRIGHT: insetsLabel = new Insets(10, 10, 10, 10); insetsMainText = new Insets(0, 10, CORNER_RADIUS + 15, 10); break; case BOTTOMLEFT: insetsLabel = new Insets(10, 10, 10, 10); insetsMainText = new Insets(0, 10, CORNER_RADIUS + 15, 10); break; case INNERRIGHT: case RIGHTTOP: insetsLabel = new Insets(10, 10, 10, CORNER_RADIUS + 15); insetsMainText = new Insets(0, 10, 10, CORNER_RADIUS + 15); break; case RIGHTBOTTOM: insetsLabel = new Insets(10, 10, 10, CORNER_RADIUS + 15); insetsMainText = new Insets(0, 10, 10, CORNER_RADIUS + 15); break; // $CASES-OMITTED$ default: } // add the headline constraints.gridx = 0; constraints.gridy = 0; constraints.insets = insetsLabel; constraints.fill = GridBagConstraints.BOTH; constraints.anchor = GridBagConstraints.WEST; constraints.gridwidth = numberAdditionalComponents > 0 ? numberAdditionalComponents : 1; constraints.weightx = 1; constraints.weighty = 0; bubble.add(headline, constraints); // create and add close Button for the Bubble constraints.gridx += numberAdditionalComponents > 0 ? numberAdditionalComponents : 1; constraints.weightx = 0; constraints.fill = GridBagConstraints.NONE; constraints.anchor = GridBagConstraints.EAST; constraints.insets = insetsLabel; constraints.gridwidth = 1; if (showCloseButton) { bubble.add(close, constraints); } else { bubble.add(new JLabel(), constraints); } // add the main Text constraints.gridx = 0; constraints.gridy += 1; constraints.insets = insetsMainText; constraints.gridwidth = numberAdditionalComponents > 0 ? numberAdditionalComponents + 1 : 2; constraints.weightx = 1; constraints.weighty = 1; constraints.fill = GridBagConstraints.BOTH; constraints.anchor = GridBagConstraints.WEST; bubble.add(mainText, constraints); // adding the given Buttons to the Bubble int insetsLeft = 10; int insetsBottom = 10; if (realAlignment == Alignment.LEFTBOTTOM || realAlignment == Alignment.LEFTTOP || realAlignment == Alignment.INNERLEFT) { insetsLeft += CORNER_RADIUS; } if (realAlignment == Alignment.BOTTOMLEFT || realAlignment == Alignment.BOTTOMRIGHT) { insetsBottom += CORNER_RADIUS; } constraints.gridx = 0; constraints.gridy += 1; constraints.insets = new Insets(10, insetsLeft, insetsBottom, 0); constraints.fill = GridBagConstraints.NONE; constraints.gridwidth = 1; constraints.weightx = 0; constraints.weighty = 0; for (int i = 0; i < this.componentsInBubble.length; i++) { // add button to bubble bubble.add(this.componentsInBubble[i], constraints); constraints.gridx += 1; } bubble.revalidate(); bubble.repaint(); } /** * updates the Alignment and Position and repaints the Bubble * * @param reregisterListeners * if true the listeners will be removed and added again after the repaint */ protected void paintAgain(final boolean reregisterListeners) { Alignment newAlignment = this.calculateAlignment(realAlignment); if (realAlignment.equals(newAlignment)) { this.pointAtComponent(); return; } else { realAlignment = newAlignment; } if (reregisterListeners) { this.unregisterRegularListener(); } layoutBubble(); pack(); positionRelative(); // repaint the entire dialog because otherwise the previous "pointer" will not be gone if // the alignment has changed // that would result in 2 pointers being displayed this.repaint(); } /** * * Adds a {@link BubbleListener}. * * @param l * The listener */ public void addBubbleListener(final BubbleListener l) { listeners.add(l); } /** * removes the given {@link BubbleListener}. * * @param l * {@link BubbleListener} to remove. */ public void removeBubbleListener(final BubbleListener l) { listeners.remove(l); } /** * Creates a speech bubble-shaped Shape. * * @param alignment * The alignment of the pointer. * * @return A speech-bubble <b>Shape</b>. */ public Shape createShape(final Alignment alignment, final float stroke) { float w = getSize().width - 2 * CORNER_RADIUS - stroke; float h = getSize().height - 2 * CORNER_RADIUS - stroke; float o = CORNER_RADIUS; GeneralPath gp = new GeneralPath(); switch (alignment) { case TOPLEFT: gp.moveTo(0, 0); gp.lineTo(0, h + o); gp.quadTo(0, h + 2 * o, o, h + 2 * o); gp.lineTo(w + o, h + 2 * o); gp.quadTo(w + 2 * o, h + 2 * o, w + 2 * o, h + o); gp.lineTo(w + 2 * o, 2 * o); gp.quadTo(w + 2 * o, o, w + o, o); gp.lineTo(o, o); gp.closePath(); break; case TOPRIGHT: gp.moveTo(0, 2 * o); gp.lineTo(0, h + o); gp.quadTo(0, h + 2 * o, o, h + 2 * o); gp.lineTo(w + o, h + 2 * o); gp.quadTo(w + 2 * o, h + 2 * o, w + 2 * o, h + o); gp.lineTo(w + 2 * o, 0); gp.lineTo(w + o, o); gp.lineTo(o, o); gp.quadTo(0, o, 0, 2 * o); break; case BOTTOMLEFT: gp.moveTo(0, o); gp.lineTo(0, h + 2 * o); gp.lineTo(o, h + o); gp.lineTo(w + o, h + o); gp.quadTo(w + 2 * o, h + o, w + 2 * o, h); gp.lineTo(w + 2 * o, o); gp.quadTo(w + 2 * o, 0, w + o, 0); gp.lineTo(o, 0); gp.quadTo(0, 0, 0, o); break; case BOTTOMRIGHT: gp.moveTo(0, o); gp.lineTo(0, h); gp.quadTo(0, h + o, o, h + o); gp.lineTo(w + o, h + o); gp.lineTo(w + 2 * o, h + 2 * o); gp.lineTo(w + 2 * o, o); gp.quadTo(w + 2 * o, 0, w + o, 0); gp.lineTo(o, 0); gp.quadTo(0, 0, 0, o); break; case LEFTBOTTOM: gp.moveTo(0, h + 2 * o); gp.lineTo(w + o, h + 2 * o); gp.quadTo(w + 2 * o, h + 2 * o, w + 2 * o, h + o); gp.lineTo(w + 2 * o, o); gp.quadTo(w + 2 * o, 0, w + o, 0); gp.lineTo(2 * o, 0); gp.quadTo(o, 0, o, o); gp.lineTo(o, h + o); gp.closePath(); break; case INNERLEFT: case LEFTTOP: gp.moveTo(0, 0); gp.lineTo(o, o); gp.lineTo(o, h + o); gp.quadTo(o, h + 2 * o, 2 * o, h + 2 * o); gp.lineTo(w + o, h + 2 * o); gp.quadTo(w + 2 * o, h + 2 * o, w + 2 * o, h + o); gp.lineTo(w + 2 * o, o); gp.quadTo(w + 2 * o, 0, w + o, 0); gp.lineTo(0, 0); break; case RIGHTBOTTOM: gp.moveTo(0, h + o); gp.quadTo(0, h + 2 * o, o, h + 2 * o); gp.lineTo(w + 2 * o, h + 2 * o); gp.lineTo(w + o, h + o); gp.lineTo(w + o, o); gp.quadTo(w + o, 0, w, 0); gp.lineTo(o, 0); gp.quadTo(0, 0, 0, o); gp.lineTo(0, h + o); break; case INNERRIGHT: case RIGHTTOP: gp.moveTo(o, 0); gp.quadTo(0, 0, 0, o); gp.lineTo(0, h + o); gp.quadTo(0, h + 2 * o, o, h + 2 * o); gp.lineTo(w, h + 2 * o); gp.quadTo(w + o, h + 2 * o, w + o, h + o); gp.lineTo(w + o, o); gp.lineTo(w + 2 * o, 0); gp.lineTo(o, 0); break; case MIDDLE: gp.moveTo(o, 0); gp.quadTo(0, 0, 0, o); gp.lineTo(0, h + o); gp.quadTo(0, h + 2 * o, o, h + 2 * o); gp.lineTo(w + o, h + 2 * o); gp.quadTo(w + 2 * o, h + 2 * o, w + 2 * o, h + o); gp.lineTo(w + 2 * o, o); gp.quadTo(w + 2 * o, 0, w + o, 0); gp.lineTo(o, 0); break; default: } AffineTransform tx = new AffineTransform(); return gp.createTransformedShape(tx); } /** * places the {@link BubbleWindow} relative to the Component which was given and adds the * listeners. */ private void positionRelative() { pointAtComponent(); registerRegularListener(); } /** * places the Bubble-speech so that it points to the Component */ protected void pointAtComponent() { double targetx = 0; double targety = 0; Point target = new Point(0, 0); if (realAlignment == Alignment.MIDDLE) { targetx = getOwner().getWidth() * 0.5 - getWidth() * 0.5; targety = getOwner().getHeight() * 0.5 - getHeight() * 0.5; } else { Point location = this.getObjectLocation(); if (location == null) { return; } int xposObject = (int) location.getX(); int yposObject = (int) location.getY(); int height = this.getObjectHeight(); int width = this.getObjectWidth(); switch (realAlignment) { case TOPLEFT: targetx = xposObject + 0.5 * width; targety = yposObject + height; break; case TOPRIGHT: targetx = xposObject + 0.5 * width - getWidth(); targety = yposObject + height; break; case LEFTBOTTOM: targetx = xposObject + width; targety = yposObject + 0.5 * height - getHeight(); break; case LEFTTOP: targetx = xposObject + width; targety = yposObject + 0.5 * height; break; case RIGHTBOTTOM: targetx = xposObject - getWidth(); targety = yposObject + 0.5 * height - getHeight(); break; case RIGHTTOP: targetx = xposObject - getWidth(); targety = yposObject + 0.5 * height; break; case BOTTOMLEFT: targetx = xposObject + 0.5 * width; targety = yposObject - getHeight(); break; case BOTTOMRIGHT: targetx = xposObject + 0.5 * width - getWidth(); targety = yposObject - getHeight(); break; case INNERLEFT: targetx = xposObject + width - 0.5 * getWidth(); double xShift = targetx + getWidth() - (getOwner().getX() + getOwner().getWidth()); if (xShift > 0) { targetx -= xShift; } targety = yposObject + height - 0.5 * getHeight(); double yShift = targety + getHeight() - (getOwner().getY() + getOwner().getHeight()); if (yShift > 0) { targety -= yShift; } break; case INNERRIGHT: targetx = xposObject - 0.5 * getWidth(); xShift = getOwner().getX() - targetx; if (xShift > 0) { targetx += xShift; } targety = yposObject + height - 0.5 * getHeight(); yShift = targety + getHeight() + 25 - (getOwner().getY() + getOwner().getHeight()); if (yShift > 0) { targety -= yShift; } break; // $CASES-OMITTED$ default: } } target = new Point((int) Math.round(targetx), (int) Math.round(targety)); setLocation(target); } /** * method to get to know whether the dockable with the given key is on Screen * * @param dockableKey * i18nKey of the wanted Dockable * @return returns 1 if the Dockable is on the Screen but not showing, -1 if the Dockable is not * on the Screen and 0 if the Dockable is on Screen and showing. */ public static int isDockableOnScreen(final String dockableKey) { Dockable dock = RapidMinerGUI.getMainFrame().getDockingDesktop().getContext().getDockableByKey(dockableKey); DockableState state = RapidMinerGUI.getMainFrame().getDockingDesktop().getDockableState(dock); if (!state.isClosed()) { if (dock.getComponent().isShowing()) { return OBJECT_SHOWING_ON_SCREEN; } return OBJECT_NOT_SHOWING; } return OBJECT_NOT_ON_SCREEN; } /** * method to get to know whether the AbstractButton with the given key is on Screen * * @param dockableKey * i18nKey of the wanted AbstractButton * @return returns 0 if the AbstractButton is on the Screen, 1 if the AbstractButton is on * Screen but the user can not see it with the current settings of the perspective and * -1 if the AbstractButton is not on the Screen. */ public static int isButtonOnScreen(final String buttonKey) { // find the Button and return -1 if we can not find it Component onScreen; try { onScreen = BubbleWindow.findButton(buttonKey, RapidMinerGUI.getMainFrame()); } catch (NullPointerException e) { return OBJECT_NOT_ON_SCREEN; } if (onScreen == null) { return OBJECT_NOT_ON_SCREEN; } // detect whether the Button is viewable int xposition = onScreen.getLocationOnScreen().x; int yposition = onScreen.getLocationOnScreen().y; int otherXposition = xposition + onScreen.getWidth(); int otherYposition = yposition + onScreen.getHeight(); Window frame = RapidMinerGUI.getMainFrame(); if (otherXposition <= frame.getWidth() && otherYposition <= frame.getHeight() && xposition > 0 && yposition > 0) { return OBJECT_SHOWING_ON_SCREEN; } else { return OBJECT_NOT_SHOWING; } } /** * @param name * i18nKey of the Button * @param searchRoot * {@link Component} in which will be searched for the Button * @return returns the {@link AbstractButton} or null if the Button was not found. */ public static AbstractButton findButton(final String name, final Component searchRoot) { if (searchRoot instanceof AbstractButton) { AbstractButton b = (AbstractButton) searchRoot; if (b.getAction() instanceof ResourceAction) { String id = (String) b.getAction().getValue("rm_id"); if (name.equals(id)) { return b; } } } if (searchRoot instanceof Container) { Component[] all = ((Container) searchRoot).getComponents(); for (Component child : all) { AbstractButton result = findButton(name, child); if (result != null) { return result; } } } return null; } /** initiate all regular Listeners */ private void initRegularListener() { perspectiveListener = new PerspectiveChangeListener() { @Override public void perspectiveChangedTo(final Perspective perspective) { if (!BubbleWindow.this.myPerspective.equals(perspective.getName())) { BubbleWindow.this.changeToAssistant(AssistantType.WRONG_PERSPECTIVE); } } }; compListener = new ComponentListener() { @Override public void componentShown(final ComponentEvent e) { BubbleWindow.this.pointAtComponent(); BubbleWindow.this.setVisible(true); } @Override public void componentResized(final ComponentEvent e) { if (BubbleWindow.this.realAlignment.equals(BubbleWindow.this.calculateAlignment(realAlignment))) { BubbleWindow.this.pointAtComponent(); } else { BubbleWindow.this.paintAgain(false); } BubbleWindow.this.setVisible(true); } @Override public void componentMoved(final ComponentEvent e) { if (BubbleWindow.this.realAlignment.equals(BubbleWindow.this.calculateAlignment(realAlignment))) { BubbleWindow.this.pointAtComponent(); } else { BubbleWindow.this.paintAgain(true); } BubbleWindow.this.setVisible(true); } @Override public void componentHidden(final ComponentEvent e) { BubbleWindow.this.setVisible(false); } }; hierachyListener = new HierarchyListener() { @Override public void hierarchyChanged(final HierarchyEvent e) { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { DockableState state = desktop.getDockableState(dockable); if (state != null && !state.isHidden() && !dockable.getComponent().isShowing() && dockable.getComponent().getParent() != null && dockable.getComponent().getParent().getParent() != null) { BubbleWindow.this.changeToAssistant(AssistantType.NOT_SHOWING); } } }); } }; dockListener = new DockingActionListener() { @Override public void dockingActionPerformed(final DockingActionEvent event) { // actionType 2 indicates that a Dockable was splitted (ACTION_SPLIT_DOCKABLE) // actionType 3 indicates that the Dockable has created his own position // (ACTION_SPLIT_COMPONENT) // actionType 5 indicates that the Dockable was docked to another position // (ACTION_CREATE_TAB) // actionType 6 indicates that the Dockable was separated (ACTION_STATE_CHANGE) if (event.getActionType() == DockingActionEvent.ACTION_CREATE_TAB || event.getActionType() == DockingActionEvent.ACTION_SPLIT_COMPONENT) { if (++dockingCounter % 2 == 0) { // repaint BubbleWindow.this.paintAgain(false); BubbleWindow.this.setVisible(true); } } if (event.getActionType() == DockingActionEvent.ACTION_STATE_CHANGE || event.getActionType() == DockingActionEvent.ACTION_SPLIT_DOCKABLE) { if (desktop.getDockableState(dockable).isHidden()) { // tab is minimized BubbleWindow.this.changeToAssistant(AssistantType.HIDDEN); } else { // repaint BubbleWindow.this.paintAgain(false); BubbleWindow.this.setVisible(true); } } if (event.getActionType() == DockingActionEvent.ACTION_CLOSE) { if (desktop.getDockableState(dockable) == null || desktop.getDockableState(dockable).isClosed()) { BubbleWindow.this.changeToAssistant(AssistantType.NOT_ON_SCREEN); } } } @Override public boolean acceptDockingAction(final DockingActionEvent arg0) { // no need to deny anything return true; } }; windowListener = new WindowAdapter() { @Override public void windowStateChanged(WindowEvent e) { // hide after iconification adn restore after deiconification // this bitwise operation tests if the new window status is iconified if ((e.getNewState() & Frame.ICONIFIED) != 0) { BubbleWindow.this.setVisible(false); } else { BubbleWindow.this.pointAtComponent(); BubbleWindow.this.setVisible(true); } } }; componentListenerToWindow = new ComponentListener() { @Override public void componentShown(final ComponentEvent e) { BubbleWindow.this.paint(false); BubbleWindow.this.setVisible(true); } @Override public void componentResized(final ComponentEvent e) { BubbleWindow.this.paint(false); BubbleWindow.this.setVisible(true); } @Override public void componentMoved(final ComponentEvent e) { BubbleWindow.this.paint(false); BubbleWindow.this.setVisible(true); } @Override public void componentHidden(final ComponentEvent e) { BubbleWindow.this.setVisible(false); } }; } /** * registers all possible Listeners(regular and the special Listeners of the subclasses) */ protected void registerRegularListener() { if (!listenersAdded) { listenersAdded = true; this.registerSpecificListener(); RapidMinerGUI.getMainFrame().addWindowStateListener(windowListener); RapidMinerGUI.getMainFrame().addComponentListener(componentListenerToWindow); if (preferredAlignment == AlignedSide.MIDDLE) { if (addPerspective) { RapidMinerGUI.getMainFrame().getPerspectiveController().getModel() .addPerspectiveChangeListener(perspectiveListener); } RapidMinerGUI.getMainFrame().addComponentListener(compListener); } else { if (addPerspective) { RapidMinerGUI.getMainFrame().getPerspectiveController().getModel() .addPerspectiveChangeListener(perspectiveListener); } if (docKey == null) { // no component was attached but possible there are some side effects RapidMinerGUI.getMainFrame().addComponentListener(compListener); } else { BubbleWindow.this.dockable.getComponent().addComponentListener(compListener); dockable.getComponent().addHierarchyListener(hierachyListener); desktop.addDockingActionListener(dockListener); } } } } /** * unregisters the regular Listeners and the special Listeners of the subclasses */ protected void unregisterRegularListener() { if (listenersAdded) { this.unregisterSpecificListeners(); RapidMinerGUI.getMainFrame().removeWindowStateListener(windowListener); RapidMinerGUI.getMainFrame().removeComponentListener(componentListenerToWindow); if (preferredAlignment == AlignedSide.MIDDLE) { if (addPerspective) { RapidMinerGUI.getMainFrame().getPerspectiveController().getModel() .removePerspectiveChangeListener(perspectiveListener); } RapidMinerGUI.getMainFrame().removeComponentListener(compListener); } else { if (addPerspective) { RapidMinerGUI.getMainFrame().getPerspectiveController().getModel() .removePerspectiveChangeListener(perspectiveListener); } if (docKey == null) { RapidMinerGUI.getMainFrame().removeComponentListener(compListener); } else { BubbleWindow.this.dockable.getComponent().removeComponentListener(compListener); dockable.getComponent().removeHierarchyListener(hierachyListener); desktop.removeDockingActionListener(dockListener); } } listenersAdded = false; } } /** * removes the Actionlistener from the close-Button of the Bubble */ private void unregister() { if (close != null) { close.removeActionListener(listener); } } /** * notifies the {@link BubbleListener}s and disposes the Bubble-speech. */ public void triggerFire() { fireEventActionPerformed(); } /** * closes the Bubble and calls bubbleClosed() of the {@link BubbleListener} */ protected void fireEventCloseClicked() { LinkedList<BubbleListener> listenerList = new LinkedList<>(listeners); this.unregister(); for (BubbleListener l : listenerList) { l.bubbleClosed(this); } unregisterRegularListener(); closeAssistants(); this.dispose(); } /** * Kills the bubble in the EDT. * * @param notifyListeners * if <code>false</code>, does <strong>not</strong> notify listeners about this */ public void killBubble(final boolean notifyListeners) { if (SwingUtilities.isEventDispatchThread()) { kill(notifyListeners); } else { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { kill(notifyListeners); } }); } } @Override public void setVisible(boolean b) { // don't show killed bubbles or bubbles in wrong perspective if (b && (killed || !isRightPerspective())) { return; } super.setVisible(b); } /** * Returns <code>true</code> if the bubble has been killed via {@link #killBubble(boolean)}. * * @return */ public boolean isKilled() { return killed; } /** * closes the Bubble and calls actionPerformed() of the {@link BubbleListener} */ protected void fireEventActionPerformed() { LinkedList<BubbleListener> listenerList = new LinkedList<>(listeners); for (BubbleListener l : listenerList) { l.actionPerformed(this); } unregisterRegularListener(); unregister(); closeAssistants(); this.dispose(); } /** * calculates the Alignment in the way, that the Bubble do not leave the Window * * @param currentAlignment * the current Alignment which will be tried to keep if the preferred Alignment is * not possible * @return returns the calculated {@link Alignment} */ protected Alignment calculateAlignment(final Alignment currentAlignment) { if (AlignedSide.MIDDLE == this.preferredAlignment) { return Alignment.MIDDLE; } if (dockable != null && (desktop.getDockableState(dockable) == null || desktop.getDockableState(dockable).isHidden())) { return Alignment.LEFTTOP; } // get Mainframe location Point frameLocation = getOwner().getLocationOnScreen(); double xlocFrame = frameLocation.getX(); double ylocFrame = frameLocation.getY(); // get Mainframe size int frameWidth = getOwner().getWidth(); int frameHeight = getOwner().getHeight(); // location and size of Component the want to attach to Point objectLocation = this.getObjectLocation(); if (objectLocation == null) { return realAlignment; } double xlocComponent = objectLocation.getX(); double ylocComponent = objectLocation.getY(); int componentWidth = this.getObjectWidth(); int componentHeight = this.getObjectHeight(); // load height and width or the approximate Value of worst case double bubbleWidth = this.getWidth(); double bubbleHeight = this.getHeight(); if (bubbleWidth == 0 || bubbleHeight == 0) { bubbleWidth = 326; bubbleHeight = 200; } if (currentAlignment == Alignment.TOPLEFT || currentAlignment == Alignment.TOPRIGHT || currentAlignment == Alignment.BOTTOMLEFT || currentAlignment == Alignment.BOTTOMRIGHT) { bubbleWidth += BUBBLE_CONNECTOR_WIDTH; } else { bubbleHeight += BUBBLE_CONNECTOR_HEIGHT; } // 0 = space above the component // 1 = space right of the component // 2 = space below the component // 3 = space left of the Component double space[] = new double[4]; space[0] = (ylocComponent - ylocFrame) / bubbleHeight; space[1] = (frameWidth + xlocFrame - (xlocComponent + componentWidth)) / bubbleWidth; space[2] = (frameHeight + ylocFrame - (ylocComponent + componentHeight)) / bubbleHeight; space[3] = (xlocComponent - xlocFrame) / bubbleWidth; // check if the preferred Alignment is valid and take it if it is valid switch (this.preferredAlignment) { case BOTTOM: if (space[2] > 1) { return this.fineTuneAlignment(Alignment.TOPLEFT, frameWidth, frameHeight, frameLocation, objectLocation, componentWidth, componentHeight); } break; case RIGHT: if (space[1] > 1) { return this.fineTuneAlignment(Alignment.LEFTBOTTOM, frameWidth, frameHeight, frameLocation, objectLocation, componentWidth, componentHeight); } break; case LEFT: if (space[3] > 1) { return this.fineTuneAlignment(Alignment.RIGHTBOTTOM, frameWidth, frameHeight, frameLocation, objectLocation, componentWidth, componentHeight); } break; case TOP: if (space[0] > 1) { return this.fineTuneAlignment(Alignment.BOTTOMLEFT, frameWidth, frameHeight, frameLocation, objectLocation, componentWidth, componentHeight); } break; // $CASES-OMITTED$ default: } // preferred Alignment was not valid. try to show bubble at the same position as before if (currentAlignment != null) { switch (currentAlignment) { case BOTTOMRIGHT: case BOTTOMLEFT: if (space[0] > 1) { return this.fineTuneAlignment(Alignment.BOTTOMLEFT, frameWidth, frameHeight, frameLocation, objectLocation, componentWidth, componentHeight); } break; case LEFTTOP: case LEFTBOTTOM: if (space[1] > 1) { return this.fineTuneAlignment(Alignment.LEFTBOTTOM, frameWidth, frameHeight, frameLocation, objectLocation, componentWidth, componentHeight); } break; case TOPRIGHT: case TOPLEFT: if (space[2] > 1) { return this.fineTuneAlignment(Alignment.TOPLEFT, frameWidth, frameHeight, frameLocation, objectLocation, componentWidth, componentHeight); } break; case RIGHTTOP: case RIGHTBOTTOM: if (space[3] > 1) { return this.fineTuneAlignment(Alignment.RIGHTBOTTOM, frameWidth, frameHeight, frameLocation, objectLocation, componentWidth, componentHeight); } break; case INNERRIGHT: case INNERLEFT: if (space[0] > 1) { return this.fineTuneAlignment(Alignment.BOTTOMLEFT, frameWidth, frameHeight, frameLocation, objectLocation, componentWidth, componentHeight); } else if (space[1] > 1) { return this.fineTuneAlignment(Alignment.LEFTBOTTOM, frameWidth, frameHeight, frameLocation, objectLocation, componentWidth, componentHeight); } else if (space[2] > 1) { return this.fineTuneAlignment(Alignment.TOPLEFT, frameWidth, frameHeight, frameLocation, objectLocation, componentWidth, componentHeight); } else if (space[3] > 1) { return this.fineTuneAlignment(Alignment.RIGHTBOTTOM, frameWidth, frameHeight, frameLocation, objectLocation, componentWidth, componentHeight); } else { return realAlignment; } // $CASES-OMITTED$ default: throw new IllegalStateException( "this part of code should be unreachable for this state of BubbleWindow"); } } if (space[1] > 1) { return this.fineTuneAlignment(Alignment.LEFTTOP, frameWidth, frameHeight, frameLocation, objectLocation, componentWidth, componentHeight); } // can not keep the old alignment. take the best fitting place int pointer = 0; for (int i = 1; i < space.length; i++) { if (space[i] > space[pointer]) { pointer = i; } } if (space[pointer] > 1) { switch (pointer) { case 0: return this.fineTuneAlignment(Alignment.BOTTOMLEFT, frameWidth, frameHeight, frameLocation, objectLocation, componentWidth, componentHeight); case 1: return this.fineTuneAlignment(Alignment.LEFTTOP, frameWidth, frameHeight, frameLocation, objectLocation, componentWidth, componentHeight); case 2: return this.fineTuneAlignment(Alignment.TOPLEFT, frameWidth, frameHeight, frameLocation, objectLocation, componentWidth, componentHeight); case 3: return this.fineTuneAlignment(Alignment.RIGHTTOP, frameWidth, frameHeight, frameLocation, objectLocation, componentWidth, componentHeight); default: throw new RuntimeException("Could not find Alignment because index was out of bound"); } } else { // can not place Bubble outside of the component so we take the right side of the inner // of the Component. return this.fineTuneAlignment(Alignment.INNERLEFT, frameWidth, frameHeight, frameLocation, objectLocation, componentWidth, componentHeight); } } /** * after the calculateAlignment() decided the optimal side, this method decides which is the * optimal direction for the Bubble * * @param firstCompute * first computed Alignment * @param xframe * width of the owner * @param yframe * height of the owner * @param frameLocation * location of the origin of the owner * @param componentLocation * location of the origin of the Component to attach to * @param compWidth * width of the component to attach to * @param compHeight * height of the component to attach to * @return the optimal Alignment in this situation */ private Alignment fineTuneAlignment(final Alignment firstCompute, final int xframe, final int yframe, final Point frameLocation, final Point componentLocation, final int compWidth, final int compHeight) { switch (firstCompute) { case TOPLEFT: case TOPRIGHT: if (componentLocation.x - frameLocation.x + compWidth / 2 > xframe / 2) { return Alignment.TOPRIGHT; } else { return Alignment.TOPLEFT; } case LEFTBOTTOM: case LEFTTOP: if (componentLocation.y - frameLocation.y + compHeight / 2 > yframe / 2) { return Alignment.LEFTBOTTOM; } else { return Alignment.LEFTTOP; } case RIGHTBOTTOM: case RIGHTTOP: if (componentLocation.y - frameLocation.y + compHeight / 2 > yframe / 2) { return Alignment.RIGHTBOTTOM; } else { return Alignment.RIGHTTOP; } case BOTTOMLEFT: case BOTTOMRIGHT: if (componentLocation.x - frameLocation.x + compWidth / 2 > xframe / 2) { return Alignment.BOTTOMRIGHT; } else { return Alignment.BOTTOMLEFT; } // $CASES-OMITTED$ default: if (realAlignment == Alignment.INNERLEFT || realAlignment == Alignment.INNERRIGHT) { return realAlignment; } if (componentLocation.x - frameLocation.x > xframe + frameLocation.x - (compWidth + componentLocation.x)) { return Alignment.INNERRIGHT; } else { return Alignment.INNERLEFT; } } } /** * indicates whether the Perspective Listener should be added or not * * @param addListener * true if the PerspectiveListener should be added and false if not */ protected void setAddPerspectiveListener(final boolean addListener) { this.addPerspective = addListener; if (perspectiveListener != null) { RapidMinerGUI.getMainFrame().getPerspectiveController().getModel() .removePerspectiveChangeListener(perspectiveListener); } } /** * returns the location of the Object the Bubble should attach to * * @return the Point that indicates the left upper corner of the Object the Bubble should point * to */ protected abstract Point getObjectLocation(); /** * method to get the width of the Object the Bubble should attach to * * @return the width of the Object */ protected abstract int getObjectWidth(); /** * method to get the height of the Object the Bubble should attach to * * @return the height of the Object */ protected abstract int getObjectHeight(); /** * unregister the components specific listeners defined in the subclasses */ protected abstract void unregisterSpecificListeners(); /** register the components specific listeners defined in the subclasses */ protected abstract void registerSpecificListener(); /** * creates an Assistant-Bubble which pauses the current Step-Bubble. Every Bubble can only have * one Assistant but an Assistant can have an Assistant too. * * @param type * of the Assistant you want to create */ protected void changeToAssistant(final AssistantType type) { if (assistantBubble == null && currentAssistant != AssistantType.NO_ASSISTANT_ACTIVE || assistantBubble != null && currentAssistant == AssistantType.NO_ASSISTANT_ACTIVE) { currentAssistant = AssistantType.NO_ASSISTANT_ACTIVE; assistantBubble = null; } if (currentAssistant != AssistantType.NO_ASSISTANT_ACTIVE || assistantBubble != null) { return; } // leftover listener fires with a delay, prevent popup if (killed) { return; } this.setVisible(false); this.unregisterRegularListener(); switch (type) { case NOT_SHOWING: assistantBubble = new DockableBubble(getOwner(), AlignedSide.RIGHT, KEY_NOT_SHOWING, docKey, new Object[] { dockable.getDockKey().getName() }); if (dockable != null) { assistantHierarchy = new HierarchyListener() { @Override public void hierarchyChanged(final HierarchyEvent e) { if (BubbleWindow.this.dockable.getComponent().isShowing()) { BubbleWindow.this.changeToMainBubble(); } } }; dockable.getComponent().addHierarchyListener(assistantHierarchy); } break; case NOT_ON_SCREEN: assistantBubble = new DockableBubble(getOwner(), AlignedSide.MIDDLE, KEY_NOT_ON_SCREEN, docKey, new Object[] { dockable.getDockKey().getName() }); assistantDockStateChange = new DockableStateChangeListener() { @Override public void dockableStateChanged(final DockableStateChangeEvent changed) { if (changed.getNewState().getDockable().getDockKey().getKey().equals(docKey) && !changed.getNewState().isClosed()) { BubbleWindow.this.changeToMainBubble(); } } }; desktop.getContext().addDockableStateChangeListener(assistantDockStateChange); assistantDockSelect = new DockableSelectionListener() { @Override public void selectionChanged(final DockableSelectionEvent arg0) { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { if (dockable.getComponent().isShowing()) { BubbleWindow.this.changeToMainBubble(); } } }); } }; desktop.addDockableSelectionListener(assistantDockSelect); break; case HIDDEN: assistantBubble = new DockableBubble(getOwner(), AlignedSide.RIGHT, KEY_HIDDEN, docKey, dockable.getDockKey().getName()); assistantDockingAction = new DockingActionListener() { @Override public void dockingActionPerformed(final DockingActionEvent arg0) { if (!desktop.getDockableState(dockable).isHidden()) { BubbleWindow.this.changeToMainBubble(); } } @Override public boolean acceptDockingAction(final DockingActionEvent arg0) { // nothing to deny return true; } }; desktop.addDockingActionListener(assistantDockingAction); assistantHierarchy = new HierarchyListener() { @Override public void hierarchyChanged(final HierarchyEvent e) { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { DockableState state = desktop.getDockableState(dockable); if (state != null && state.isHidden()) { assistantBubble.paint(false); } else if (state != null) { // this case means that the user restored the perspective BubbleWindow.this.changeToMainBubble(); } } }); } }; dockable.getComponent().addHierarchyListener(assistantHierarchy); break; case WRONG_PERSPECTIVE: String buttonKey = ""; if (myPerspective.equals("design")) { buttonKey = "workspace_design"; } else if (myPerspective.equals("result")) { buttonKey = "workspace_result"; } assistantBubble = new ButtonBubble(getOwner(), null, AlignedSide.BOTTOM, KEY_PERSPECTIVE, buttonKey, false, false, new Object[] { myPerspective }); assistantPerspective = new PerspectiveChangeListener() { @Override public void perspectiveChangedTo(final Perspective perspective) { if (BubbleWindow.this.myPerspective.equals(perspective.getName())) { BubbleWindow.this.changeToMainBubble(); } } }; RapidMinerGUI.getMainFrame().getPerspectiveController().getModel() .addPerspectiveChangeListener(assistantPerspective); break; case NOT_IN_CHAIN: case NO_ASSISTANT_ACTIVE: // will not be called here break; default: throw new IllegalArgumentException( "the AssistantType " + type.toString() + " is not supported by this class"); } currentAssistant = type; assistantBubble.addBubbleListener(new BubbleListener() { @Override public void bubbleClosed(final BubbleWindow bw) { BubbleWindow.this.fireEventCloseClicked(); } @Override public void actionPerformed(final BubbleWindow bw) { // do not care } }); assistantBubble.setVisible(true); } /** * closes the current Assistant and restarts the current Step */ protected void changeToMainBubble() { if (currentAssistant == AssistantType.NO_ASSISTANT_ACTIVE) { return; } switch (currentAssistant) { case NOT_SHOWING: closeShowingAssistant(); break; case NOT_ON_SCREEN: closeNotOnScreenAssistant(); break; case HIDDEN: closeHiddenAssistant(); break; case WRONG_PERSPECTIVE: closePerspectiveAssistant(); break; case NOT_IN_CHAIN: case NO_ASSISTANT_ACTIVE: default: } this.registerRegularListener(); this.paint(false); this.setVisible(true); } /** * closes the WRONG_PERSPECTIVE-Assistant */ private void closePerspectiveAssistant() { if (assistantBubble != null && currentAssistant == AssistantType.WRONG_PERSPECTIVE) { assistantBubble.triggerFire(); assistantBubble = null; RapidMinerGUI.getMainFrame().getPerspectiveController().getModel() .removePerspectiveChangeListener(assistantPerspective); currentAssistant = AssistantType.NO_ASSISTANT_ACTIVE; } } /** * closes the NOTSHOWING-Assistant */ private void closeShowingAssistant() { if (assistantBubble != null && currentAssistant == AssistantType.NOT_SHOWING) { assistantBubble.triggerFire(); assistantBubble = null; dockable.getComponent().removeHierarchyListener(assistantHierarchy); currentAssistant = AssistantType.NO_ASSISTANT_ACTIVE; } } /** * closes the NOTONSCREEN-Assistant */ private void closeNotOnScreenAssistant() { if (assistantBubble != null && currentAssistant == AssistantType.NOT_ON_SCREEN) { assistantBubble.triggerFire(); assistantBubble = null; desktop.getContext().removeDockableStateChangeListener(assistantDockStateChange); desktop.removeDockableSelectionListener(assistantDockSelect); currentAssistant = AssistantType.NO_ASSISTANT_ACTIVE; } } /** * closes the HIDDEN-Assistant */ private void closeHiddenAssistant() { if (assistantBubble != null && currentAssistant == AssistantType.HIDDEN) { assistantBubble.triggerFire(); assistantBubble = null; desktop.removeDockingActionListener(assistantDockingAction); dockable.getComponent().removeHierarchyListener(assistantHierarchy); currentAssistant = AssistantType.NO_ASSISTANT_ACTIVE; } } /** * closes every Assistant does not matter which is active */ protected void closeAssistants() { closeShowingAssistant(); closeNotOnScreenAssistant(); closeHiddenAssistant(); closePerspectiveAssistant(); } protected BubbleWindow getAssistantBubble() { return assistantBubble; } protected void setAssistantBubble(final DockableBubble newAssistant) { assistantBubble = newAssistant; } /** returns which Assistant is currently active */ protected AssistantType getCurrentAssistantType() { return currentAssistant; } /** sets the currentAssistant to the given value */ protected void setCurrentAssistantType(final AssistantType newType) { if (newType == null) { throw new IllegalArgumentException("parameter can not be null, please choose NO_ASSISTANT instead"); } currentAssistant = newType; } /** * The real alignment of the bubble, indicating where the pointer is, as opposed to the * positioning on within the window. */ protected Alignment getRealAlignment() { return realAlignment; } /** the direct or indirect Dockable which is attached by the Bubble */ protected Dockable getDockable() { return dockable; } /** the key of the used Dockable for this Bubble */ protected String getDockableKey() { return docKey; } /** the DockingDesktop of the UI */ protected DockingDesktop getDockingDesktop() { return desktop; } void addComponentListenerTo(JComponent comp) { comp.addComponentListener(compListener); } public void setHeadline(String headline) { this.headline.setText(headline); } public void setMainText(String mainText) { int width = style != BubbleStyle.COMIC ? WINDOW_WIDTH : WINDOW_WIDTH_COMIC; this.mainText.setText("<html><div style=\"line-height: 150%;width:" + width + "px \">" + mainText + "</div></html>"); } @Override public void paint(Graphics g) { // this is necessary due to a bug on Windows which involves setting the clip in the bubble // panel paintComponent() method in combination with the transparent JDialog and hovering // over a JButton // without this, the inner JPanel only repaints on a clip equal to the hovered JButton g.setClip(null); super.paint(g); } /** * Kills the bubble. * * @param notifyListeners * if <code>false</code>, does <strong>not</strong> notify listeners about this */ private void kill(boolean notifyListeners) { if (!killed) { killed = true; this.unregister(); if (notifyListeners) { LinkedList<BubbleListener> listenerList = new LinkedList<>(listeners); for (BubbleListener l : listenerList) { l.bubbleClosed(this); } } unregisterRegularListener(); closeAssistants(); this.dispose(); } } /** * @return the current {@link BubbleStyle} */ final BubbleStyle getStyle() { return style; } /** * Checks if the current perspective matches {@link #myPerspective}. * * @return {@code true} if {@link #myPerspective} is equal to * {@link #getCurrentPerspectiveName()}, otherwise {@code false} */ protected boolean isRightPerspective() { return myPerspective.equals(getCurrentPerspectiveName()); } /** * Getter for the current perspective name. * * @return The name of the current perspective. */ protected String getCurrentPerspectiveName() { return RapidMinerGUI.getMainFrame().getPerspectiveController().getModel().getSelectedPerspective().getName(); } }