/*
* RapidMiner
*
* Copyright (C) 2001-2014 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.components;
import java.awt.BasicStroke;
import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
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.ActionListener;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.event.ComponentListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
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.JDialog;
import javax.swing.JLabel;
import javax.swing.JPanel;
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.gui.tour.Step;
import com.rapidminer.tools.I18N;
import com.rapidminer.tools.LogService;
import com.rapidminer.tools.Tools;
import com.sun.awt.AWTUtilities;
import com.vlsolutions.swing.docking.Dockable;
import com.vlsolutions.swing.docking.DockableState;
import com.vlsolutions.swing.docking.DockingDesktop;
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 attache to
* Buttons, either by using its ID or by passing a reference.
* 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.
* The keys for the title and the text must be of format gui.bubble.XXX.body or gui.bubble.XXX.title .
*
* @author Philipp Kersting and Thilo Kamradt
*
*/
public abstract class BubbleWindow extends JDialog {
private static final long serialVersionUID = -6369389148455099450L;
public static interface BubbleListener {
public void bubbleClosed(BubbleWindow bw);
public void actionPerformed(BubbleWindow bw);
}
private List<BubbleListener> listeners = new LinkedList<BubbleListener>();
/** indicates on which side the Bubble will appear*/
public enum AlignedSide {
RIGHT, LEFT, TOP, BOTTOM, MIDDLE
}
/** Used to define the position of the pointer of the bubble
* (Describes the corner which points to the component).
* CENTER places the Bubble inside the Component. MIDDLE places the BubbleWindow in the middle of the mainframe( won't be checked by the BubbleWindow if chosen).
*/
private enum Alignment {
TOPLEFT, TOPRIGHT, BOTTOMLEFT, BOTTOMRIGHT, LEFTTOP, LEFTBOTTOM, RIGHTTOP, RIGHTBOTTOM, INNERRIGHT, INNERLEFT, MIDDLE;
}
private static 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);
}
private static final int CORNER_RADIUS = 20;
private static final int WINDOW_WIDTH = 200;
/** Shape used for setting the shape of the window and for rendering the outline. */
private Shape shape;
protected Alignment realAlignment;
protected AlignedSide preferredAlignment;
private JPanel bubble;
private ImageIcon background;
private JButton close;
private GridBagConstraints constraints = null;
private ImageIcon passiveCloseIcon, activCloseIcon;
private ActionListener listener;
private JLabel headline;
private JLabel mainText;
private DockableStateChangeListener stateChangeListener;
private PerspectiveChangeListener perspectiveListener = null;
private WindowAdapter windowListener;
protected Window owner;
private String myPerspective;
/** indicates whether the listeners currently are added or not */
private boolean listenersAdded = false;
private boolean addPerspective = true;
protected String docKey = null;
protected Dockable dockable;
protected ComponentListener compListener;
protected DockingActionListener dockListener = null;
protected final DockingDesktop desktop = RapidMinerGUI.getMainFrame().getDockingDesktop();
private int dockingCounter = 0;
/**
* @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 {@link Component} to which this {@link BubbleWindow} should be placed relative to.
* @param addListener indicates whether the {@link BubbleWindow} closes if the Button was pressed or when another Listener added by a subclass of {@link Step} is fired.
*/
public BubbleWindow(Window owner, final AlignedSide preferredAlignment, String i18nKey, String docKey, Object... arguments) {
super(owner);
this.owner = owner;
this.myPerspective = RapidMinerGUI.getMainFrame().getPerspectives().getCurrentPerspective().getName();
this.preferredAlignment = preferredAlignment;
if (docKey != null) {
this.docKey = docKey;
dockable = desktop.getContext().getDockableByKey(docKey);
}
//load image for background
background = new ImageIcon(Tools.getResource("/images/comic-pattern.png"));
//headline label
{
headline = new JLabel(I18N.getGUIBundle().getString("gui.bubble." + i18nKey + ".title"));
headline.setFont(new Font("AlterEgoBB", Font.PLAIN, 14).deriveFont(Font.BOLD));
headline.setMinimumSize(new Dimension(WINDOW_WIDTH, 12));
headline.setPreferredSize(new Dimension(WINDOW_WIDTH, 12));
}
//mainText label
{
mainText = new JLabel("<html><div style=\"line-height: 150%;width:" + WINDOW_WIDTH + "px \">" + I18N.getMessage(I18N.getGUIBundle(), "gui.bubble." + i18nKey + ".body", arguments) + "</div></html>");
mainText.setOpaque(false);
mainText.setFont(new Font("AlterEgoBB", Font.PLAIN, 12));
mainText.setMinimumSize(new Dimension(150, 20));
mainText.setMaximumSize(new Dimension(WINDOW_WIDTH, 800));
}
}
/**
* 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
*/
public void paint(boolean reregisterListerns) {
if(constraints == null) {
this.buildBubble();
} else {
this.paintAgain(reregisterListerns);
}
}
/**
* builds the Bubble for the first time
*/
private void buildBubble() {
this.realAlignment = this.calculateAlignment(this.realAlignment);
setLayout(new BorderLayout());
setUndecorated(true);
addComponentListener(new ComponentAdapter() {
@Override
public void componentResized(ComponentEvent e) {
shape = createShape(realAlignment);
String version = System.getProperty("java.version").substring(0, 3).replace(".", "");
try {
Integer versionNumber = Integer.valueOf(version);
if (versionNumber == 16) {
// Java SE 6 Update 10
AWTUtilities.setWindowShape(BubbleWindow.this, shape);
} else if (versionNumber >= 17) {
// Java 7+
setShape(shape);
}
} catch (Throwable t) {
LogService.getRoot().log(Level.WARNING, "Could not create shaped Bubble Windows. Error: " + t.getLocalizedMessage(), t);
}
}
});
GridBagLayout gbl = new GridBagLayout();
bubble = new JPanel(gbl) {
private static final long serialVersionUID = 1L;
@Override
protected void paintComponent(Graphics gr) {
super.paintComponent(gr);
Graphics2D g = (Graphics2D) gr;
g.setColor(SwingTools.RAPID_I_BROWN);
g.setStroke(new BasicStroke(6));
g.setRenderingHints(HI_QUALITY_HINTS);
g.drawImage(background.getImage(), 0, 0, this);
g.draw(AffineTransform.getTranslateInstance(-.5, -.5).createTransformedShape(getShape()));
}
};
bubble.setBackground(SwingTools.LIGHTEST_BLUE);
bubble.setSize(getSize());
getContentPane().add(bubble, BorderLayout.CENTER);
constraints = new GridBagConstraints();
Insets insetsLabel = new Insets(10, 10, 10, 10);
Insets insetsMainText = new Insets(0, 10, 10, 10);
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;
default:
}
//add the headline
constraints.insets = insetsLabel;
constraints.fill = GridBagConstraints.BOTH;
constraints.anchor = GridBagConstraints.FIRST_LINE_START;
constraints.weightx = 1;
constraints.weighty = 0;
constraints.gridwidth = GridBagConstraints.RELATIVE;
bubble.add(headline, constraints);
//create and add close Button for the Bubble
constraints.weightx = 0;
constraints.gridwidth = GridBagConstraints.REMAINDER;
constraints.insets = insetsLabel;
passiveCloseIcon = new ImageIcon(DockingDesktop.class.getResource("/com/vlsolutions/swing/docking/close16v2.png"));
activCloseIcon = new ImageIcon(DockingDesktop.class.getResource("/com/vlsolutions/swing/docking/close16v2rollover.png"));
close = new JButton(passiveCloseIcon);
close.setBorderPainted(false);
close.setOpaque(false);
// change Icons and set close operation
close.addMouseListener(new MouseListener() {
@Override
public void mouseReleased(MouseEvent e) {
// don't care
}
@Override
public void mousePressed(MouseEvent e) {
//don't care
}
@Override
public void mouseExited(MouseEvent e) {
BubbleWindow.this.close.setIcon(BubbleWindow.this.passiveCloseIcon);
}
@Override
public void mouseEntered(MouseEvent e) {
BubbleWindow.this.close.setIcon(BubbleWindow.this.activCloseIcon);
}
@Override
public void mouseClicked(MouseEvent e) {
BubbleWindow.this.dispose();
fireEventCloseClicked();
}
});
close.setMargin(new Insets(0, 5, 0, 5));
bubble.add(close, constraints);
//add the main Text
constraints.insets = insetsMainText;
constraints.gridwidth = GridBagConstraints.REMAINDER;
constraints.weightx = 1;
constraints.weighty = 1;
bubble.add(mainText, constraints);
pack();
if (this.calculateAlignment(this.realAlignment) == this.realAlignment) {
positionRelative();
} else {
this.paintAgain(false);
}
}
/**
* updates the Alignment and Position and repaints the Bubble
* @param reregisterListeners if true the listeners will be removed and added again after the repaint
*/
private void paintAgain(boolean reregisterListeners) {
Alignment newAlignment = this.calculateAlignment(realAlignment);
if(realAlignment.equals(newAlignment)) {
this.pointAtComponent();
return;
} else {
realAlignment = newAlignment;
}
shape = createShape(realAlignment);
if (reregisterListeners) {
this.unregisterMovementListener();
}
//choose the right call for the right version
String version = System.getProperty("java.version").substring(0, 3).replace(".", "");
try {
Integer versionNumber = Integer.valueOf(version);
if (versionNumber == 16) {
// Java SE 6 Update 10
AWTUtilities.setWindowShape(this, shape);
} else if (versionNumber >= 17) {
// Java 7+
setShape(shape);
}
} catch (Throwable t) {
LogService.getRoot().log(Level.WARNING, "Could not create shaped Bubble Windows. Error: " + t.getLocalizedMessage(), t);
}
bubble.removeAll();
Insets insetsLabel = new Insets(10, 10, 10, 10);
Insets insetsMainText = new Insets(0, 10, 10, 10);
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;
default:
}
//add headline
constraints.insets = insetsLabel;
constraints.fill = GridBagConstraints.BOTH;
constraints.anchor = GridBagConstraints.FIRST_LINE_START;
constraints.weightx = 1;
constraints.weighty = 0;
constraints.gridwidth = GridBagConstraints.RELATIVE;
bubble.add(headline, constraints);
//add close-Button
constraints.weightx = 0;
constraints.gridwidth = GridBagConstraints.REMAINDER;
constraints.insets = insetsLabel;
bubble.add(close, constraints);
//add main text
constraints.insets = insetsMainText;
constraints.gridwidth = GridBagConstraints.REMAINDER;
constraints.weightx = 1;
constraints.weighty = 1;
bubble.add(mainText, constraints);
pack();
positionRelative();
}
/**
*
* Adds a {@link BubbleListener}.
*
* @param l The listener
*/
public void addBubbleListener(BubbleListener l) {
listeners.add(l);
}
/**
* removes the given {@link BubbleListener}.
* @param l {@link BubbleListener} to remove.
*/
public void removeBubbleListener(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(Alignment alignment) {
int w = getSize().width - 2 * CORNER_RADIUS;
int h = getSize().height - 2 * CORNER_RADIUS;
int 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.lineTo(0, 0);
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();
registerMovementListener();
}
/**
* places the Bubble-speech so that it points to the Component
* @param component component to point to
*/
protected void pointAtComponent() {
double targetx = 0;
double targety = 0;
Point target = new Point(0, 0);
if (realAlignment == Alignment.MIDDLE) {
targetx = owner.getWidth() * 0.5 - getWidth() * 0.5;
targety = owner.getHeight() * 0.5 - getHeight() * 0.5;
} else {
Point location = this.getObjectLocation();
int x = (int) location.getX();
int y = (int) location.getY();
int h = this.getObjectHeight();
int w = this.getObjectWidth();
switch (realAlignment) {
case TOPLEFT:
targetx = x + 0.5 * w;
targety = y + h;
break;
case TOPRIGHT:
targetx = (x + 0.5 * w) - getWidth();
targety = y + h;
break;
case LEFTBOTTOM:
targetx = x + w;
targety = (y + 0.5 * h) - getHeight();
break;
case LEFTTOP:
targetx = x + w;
targety = (y + 0.5 * h);
break;
case RIGHTBOTTOM:
targetx = x - getWidth();
targety = (y + 0.5 * h) - getHeight();
break;
case RIGHTTOP:
targetx = x - getWidth();
targety = (y + 0.5 * h);
break;
case BOTTOMLEFT:
targetx = x + 0.5 * w;
targety = y - getHeight();
break;
case BOTTOMRIGHT:
targetx = x + 0.5 * w - getWidth();
targety = y - getHeight();
break;
case INNERLEFT:
targetx = x + w - 0.5 * getWidth();
double xShift = (targetx + getWidth()) - (owner.getX() + owner.getWidth());
if (xShift > 0) {
targetx -= xShift;
}
targety = y + h - 0.5 * getHeight();
double yShift = (targety + getHeight()) - (owner.getY() + owner.getHeight());
if (yShift > 0) {
targetx -= yShift;
}
break;
case INNERRIGHT:
targetx = x - 0.5 * getWidth();
xShift = owner.getX() - targetx;
if (xShift > 0) {
targetx += xShift;
}
targety = y + h - 0.5 * getHeight();
yShift = (targety + getHeight()) - (owner.getY() + owner.getHeight());
if (yShift > 0) {
targetx -= yShift;
}
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 and -1 if the Dockable is not on the Screen.
*/
public static int isDockableOnScreen(String dockableKey) {
Dockable onScreen = RapidMinerGUI.getMainFrame().getDockingDesktop().getContext().getDockableByKey(dockableKey);
if (onScreen == null)
return -1;
return 1;
}
/**
* method to get to know whether the AbstractButton with the given key is on Screen
* @param dockableKey i18nKey of the wanted AbstractButton
* @return returns 1 if the AbstractButton is on the Screen, 0 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(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 -1;
}
if (onScreen == null)
return -1;
// 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 1;
} else {
return 0;
}
}
/**
* @param name i18nKey of the Button
* @param searchRoot {@link Component} to search in for the Button
* @return returns the {@link AbstractButton} if found or null if the Button was not found.
*/
public static AbstractButton findButton(String name, 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;
}
/**
* Returns the {@link Shape} of this {@link BubbleWindow}
*/
public Shape getShape() {
if (shape == null) {
shape = createShape(realAlignment);
}
return shape;
}
protected void registerMovementListener() {
if (!listenersAdded) {
if (addPerspective) {
perspectiveListener = new PerspectiveChangeListener() {
@Override
public void perspectiveChangedTo(Perspective perspective) {
if ((BubbleWindow.this.myPerspective).equals(perspective.getName())) {
BubbleWindow.this.reloadComponent();
BubbleWindow.this.setVisible(true);
} else {
BubbleWindow.this.setVisible(false);
}
}
};
}
compListener = new ComponentListener() {
@Override
public void componentShown(ComponentEvent e) {
BubbleWindow.this.pointAtComponent();
BubbleWindow.this.setVisible(true);
}
@Override
public void componentResized(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(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(ComponentEvent e) {
BubbleWindow.this.setVisible(false);
}
};
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);
dockListener = new DockingActionListener() {
@Override
public void dockingActionPerformed(DockingActionEvent event) {
//TODO: use constants instead of integers and check for name first
// actionType 2 indicates that a Dockable was splitted
// actionType 3 indicates that the Dockable has created his own position
// actionType 5 indicates that the Dockable was docked to another position
// actionType 6 indicates that the Dockable was separated
if (event.getActionType() == 5 || event.getActionType() == 3) {
if ((++dockingCounter) % 2 == 0) {
//get the new component of the Dockable because the current component is disabled
BubbleWindow.this.dockable.getComponent().removeComponentListener(compListener);
BubbleWindow.this.reloadComponent();
//repaint
BubbleWindow.this.paintAgain(false);
BubbleWindow.this.setVisible(true);
}
}
if (event.getActionType() == 6 || event.getActionType() == 2) {
//get the new component of the Dockable because the current component is disabled
BubbleWindow.this.dockable.getComponent().removeComponentListener(compListener);
BubbleWindow.this.reloadComponent();
//repaint
BubbleWindow.this.paintAgain(false);
BubbleWindow.this.setVisible(true);
}
}
@Override
public boolean acceptDockingAction(DockingActionEvent arg0) {
// no need to deny anything
return true;
}
};
desktop.addDockingActionListener(dockListener);
stateChangeListener = new DockableStateChangeListener() {
@Override
public void dockableStateChanged(DockableStateChangeEvent arg0) {
DockableState state = arg0.getNewState();
if (state.isClosed()) {
//TODO: try to reload
System.out.println("---dock closed");
} else if (state.isDocked()) {
//TODO: do nothing
System.out.println("---dock docked");
} else if (state.isFloating()) {
//TODO: re attach
System.out.println("---dock floating");
} else if (state.isMaximized()) {
//TODO: set reload bubble (paint(true))
System.out.println("---dock maximized");
}
if(arg0.getNewState().getDockable().getDockKey().getKey().equals(BubbleWindow.this.docKey)) {
state = arg0.getNewState();
if (state.isClosed()) {
//TODO: try to reload
System.out.println("dock closed");
} else if (state.isDocked()) {
//TODO: do nothing
System.out.println("dock docked");
} else if (state.isFloating()) {
//TODO: re attach
System.out.println("dock floating");
} else if (state.isMaximized()) {
//TODO: set reload bubble (paint(true))
System.out.println("dock maximized");
}
switch (state.getLocation()) {
default:
break;
}
}
}
};
desktop.getContext().addDockableStateChangeListener(stateChangeListener);
}
windowListener = new WindowAdapter() {
@Override
public void windowIconified(WindowEvent e) {
super.windowIconified(e);
BubbleWindow.this.setVisible(false);
}
@Override
public void windowDeiconified(WindowEvent e) {
super.windowDeiconified(e);
BubbleWindow.this.pointAtComponent();
BubbleWindow.this.setVisible(true);
}
};
if (addPerspective) {
RapidMinerGUI.getMainFrame().getPerspectives().addPerspectiveChangeListener(perspectiveListener);
}
RapidMinerGUI.getMainFrame().addWindowStateListener(windowListener);
listenersAdded = true;
}
}
private void unregister() {
if (close != null) {
close.removeActionListener(listener);
}
}
protected void unregisterMovementListener() {
if(listenersAdded) {
if(docKey == null) {
RapidMinerGUI.getMainFrame().removeComponentListener(compListener);
} else {
BubbleWindow.this.dockable.getComponent().removeComponentListener(compListener);
desktop.removeDockingActionListener(dockListener);
}
if (addPerspective) {
RapidMinerGUI.getMainFrame().getPerspectives().removePerspectiveChangeListener(perspectiveListener);
}
RapidMinerGUI.getMainFrame().removeWindowStateListener(windowListener);
listenersAdded = false;
}
}
/**
* notifies the {@link BubbleListener}s and disposes the Bubble-speech.
*/
public void triggerFire() {
fireEventActionPerformed();
dispose();
}
protected void fireEventCloseClicked() {
LinkedList<BubbleListener> listenerList = new LinkedList<BubbleWindow.BubbleListener>(listeners);
this.unregister();
for (BubbleListener l : listenerList) {
l.bubbleClosed(this);
}
unregisterMovementListener();
}
protected void fireEventActionPerformed() {
LinkedList<BubbleListener> listenerList = new LinkedList<BubbleWindow.BubbleListener>(listeners);
for (BubbleListener l : listenerList) {
l.actionPerformed(this);
}
unregisterMovementListener();
unregister();
}
/**
* calculates the Alignment in the way, that the Bubble do not leave the Window
* @param preferredAlignment preferred Alignment of the User
* @param location Point which indicates the left upper corner of the Object to which the Bubble should point to
* @param xSize size in x-direction of the Object the Bubble should point to
* @param ySize size in y-direction of the Object the Bubble should point to
* @return returns the calculated {@link Alignment}
*/
protected Alignment calculateAlignment(Alignment currentAlignment) {
if (AlignedSide.MIDDLE == this.preferredAlignment) {
return Alignment.MIDDLE;
}
//get Mainframe location
Point frameLocation = owner.getLocationOnScreen();
double xlocFrame = frameLocation.getX();
double ylocFrame = frameLocation.getY();
//get Mainframe size
int frameWidth = owner.getWidth();
int frameHeight = owner.getHeight();
//location and size of Component the want to attach to
Point componentLocation = this.getObjectLocation();
double xlocComponent = componentLocation.getX();
double ylocComponent = componentLocation.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;
}
// TODO: after finishing design recalculate the save zone
if (currentAlignment == Alignment.TOPLEFT || currentAlignment == Alignment.TOPRIGHT || currentAlignment == Alignment.BOTTOMLEFT || currentAlignment == Alignment.BOTTOMRIGHT) {
bubbleWidth += 46;
} else {
bubbleHeight += 35;
}
// 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, componentLocation, componentWidth, componentHeight);
break;
case RIGHT:
if (space[1] > 1)
return this.fineTuneAlignment(Alignment.LEFTBOTTOM, frameWidth, frameHeight, frameLocation, componentLocation, componentWidth, componentHeight);
break;
case LEFT:
if (space[3] > 1)
return this.fineTuneAlignment(Alignment.RIGHTBOTTOM, frameWidth, frameHeight, frameLocation, componentLocation, componentWidth, componentHeight);
break;
case TOP:
if (space[0] > 1)
return this.fineTuneAlignment(Alignment.BOTTOMLEFT, frameWidth, frameHeight, frameLocation, componentLocation, componentWidth, componentHeight);
break;
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, componentLocation, componentWidth, componentHeight);
break;
case LEFTTOP:
case LEFTBOTTOM:
if (space[1] > 1)
return this.fineTuneAlignment(Alignment.LEFTBOTTOM, frameWidth, frameHeight, frameLocation, componentLocation, componentWidth, componentHeight);
break;
case TOPRIGHT:
case TOPLEFT:
if (space[2] > 1)
return this.fineTuneAlignment(Alignment.TOPLEFT, frameWidth, frameHeight, frameLocation, componentLocation, componentWidth, componentHeight);
break;
case RIGHTTOP:
case RIGHTBOTTOM:
if (space[3] > 1)
return this.fineTuneAlignment(Alignment.RIGHTBOTTOM, frameWidth, frameHeight, frameLocation, componentLocation, componentWidth, componentHeight);
break;
case INNERRIGHT:
case INNERLEFT:
if (space[0] > 1) {
return this.fineTuneAlignment(Alignment.BOTTOMLEFT, frameWidth, frameHeight, frameLocation, componentLocation, componentWidth, componentHeight);
} else if (space[1] > 1) {
return this.fineTuneAlignment(Alignment.LEFTBOTTOM, frameWidth, frameHeight, frameLocation, componentLocation, componentWidth, componentHeight);
} else if (space[2] > 1) {
return this.fineTuneAlignment(Alignment.TOPLEFT, frameWidth, frameHeight, frameLocation, componentLocation, componentWidth, componentHeight);
} else if (space[3] > 1) {
return this.fineTuneAlignment(Alignment.RIGHTBOTTOM, frameWidth, frameHeight, frameLocation, componentLocation, componentWidth, componentHeight);
} else {
// return this.fineTuneAlignment(Alignment.INNERLEFT, frameWidth, frameHeight, frameLocation, location, componentWidth, componentHeight);
return realAlignment;
}
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, componentLocation, 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, componentLocation, componentWidth, componentHeight);
case 1:
return this.fineTuneAlignment(Alignment.LEFTTOP, frameWidth, frameHeight, frameLocation, componentLocation, componentWidth, componentHeight);
case 2:
return this.fineTuneAlignment(Alignment.TOPLEFT, frameWidth, frameHeight, frameLocation, componentLocation, componentWidth, componentHeight);
case 3:
return this.fineTuneAlignment(Alignment.RIGHTTOP, frameWidth, frameHeight, frameLocation, componentLocation, componentWidth, componentHeight);
default:
return null;
}
} 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, componentLocation, componentWidth, componentHeight);
}
}
/**
* Whether we want the north-, south, west- or east-side of the {@link Component} was
* chosen before this method the decide in which direction the Bubble will expand
* @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
*/
private Alignment fineTuneAlignment(Alignment firstCompute, int xframe, int yframe, Point frameLocation, Point componentLocation, int compWidth, 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;
}
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;
}
}
}
protected void setAddPerspectiveListener(boolean addListener) {
this.addPerspective = addListener;
}
/**
* returns the location of the Object the Bubble should attach to
* @return the Point 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 returns the width of the Object
*/
protected abstract int getObjectWidth();
/**
* method to get the height of the Object the Bubble should attach to
* @return returns the height of the Object
*/
protected abstract int getObjectHeight();
/**
* deletes old listeners, updates the Components which are listened and adds the Component specific listeners again
*/
protected void reloadComponent() {
if (docKey != null) {
dockable = desktop.getContext().getDockableByKey(docKey);
BubbleWindow.this.dockable.getComponent().addComponentListener(compListener);
desktop.addDockingActionListener(dockListener);
}
}
/**
* 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();
}