/**
* 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;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Container;
import java.awt.GraphicsDevice;
import java.awt.GraphicsEnvironment;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import javax.swing.BorderFactory;
import javax.swing.JComponent;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JWindow;
import javax.swing.SwingUtilities;
import javax.swing.Timer;
import javax.swing.border.Border;
import com.rapidminer.gui.RapidMinerGUI;
/**
* This component is based on a {@link JWindow}. Once {@link #setVisible(boolean)} is called, the
* popup will display and fade away after the specified amount of time if the GraphicsDevice
* supports it. To use this component with minimal effort, you can utilize the static
* {@link #showFadingPopup(JPanel, JComponent, PopupLocation)} methods.
*
* @author Marco Boeck
*
*/
public class NotificationPopup extends JWindow {
/**
* Listener for {@link NotificationPopup}s.
*
*/
public static interface NotificationPopupListener {
/**
* Triggered when the popup is gone, either by having faded out or being closed via
* mouse-click.
*
* @param popup
*/
public void popupClosed(NotificationPopup popup);
}
/**
* Used to specifiy the location of the {@link NotificationPopup} on the parent component.
*
*/
public static enum PopupLocation {
CENTER, CENTER_RIGHT, CENTER_LEFT, UPPER_LEFT, UPPER_CENTER, UPPER_RIGHT, LOWER_LEFT, LOWER_CENTER, LOWER_RIGHT;
}
private static final long serialVersionUID = 3620229881624404350L;
/** the default time before the popup fades away in ms */
public static final int DEFAULT_DELAY = 3000;
/** the timer used to fade the popup in */
private Timer fadeInTimer;
/** the timer used to fade the popup out */
private Timer fadeOutTimer;
/** the mouse listener which allows closing of the notification with a click */
private MouseListener mouseListener;
/** the alpha value */
private float alpha;
/** flag indicating if we can fade the popup out or not */
private boolean isOpacitySupported;
/** the listener for this popup */
private NotificationPopupListener listener;
/**
* Creates a new {@link NotificationPopup} instance with the given content panel and the given
* timeout in ms before the popup fades away.
*
* @param content
* the content panel to display
* @param delay
* the time in milliseconds before the popup will start fading away after
* {@link #show(java.awt.Component, int, int)} has been called
* @param listener
* the listener which is called for notification popup events; can be
* <code>null</code>
*/
private NotificationPopup(final JPanel content, final int delay, final NotificationPopupListener listener) {
super(RapidMinerGUI.getMainFrame());
// determine what the default GraphicsDevice can support
GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
GraphicsDevice gd = ge.getDefaultScreenDevice();
isOpacitySupported = gd.isWindowTranslucencySupported(GraphicsDevice.WindowTranslucency.TRANSLUCENT);
this.listener = listener;
alpha = 0.0f;
fadeOutTimer = new Timer(10, new ActionListener() {
@Override
public void actionPerformed(final ActionEvent e) {
if (alpha <= 0.0f) {
fadeOutTimer.stop();
dispose();
return;
}
alpha = Math.max(0.0f, alpha - 0.01f);
if (isOpacitySupported) {
NotificationPopup.this.setOpacity(alpha);
}
}
});
fadeOutTimer.setInitialDelay(delay);
fadeInTimer = new Timer(10, new ActionListener() {
@Override
public void actionPerformed(final ActionEvent e) {
if (alpha >= 1.0f) {
fadeInTimer.stop();
fadeOutTimer.start();
return;
}
alpha = Math.min(1.0f, alpha + 0.10f);
if (isOpacitySupported) {
NotificationPopup.this.setOpacity(alpha);
}
}
});
fadeInTimer.setInitialDelay(0);
// allow closing of the notification with the mouse
mouseListener = new MouseAdapter() {
@Override
public void mousePressed(final MouseEvent e) {
dispose();
}
@Override
public void mouseEntered(final MouseEvent e) {
if (!fadeInTimer.isRunning()) {
alpha = 1.0f;
if (isOpacitySupported) {
NotificationPopup.this.setOpacity(alpha);
}
fadeOutTimer.stop();
}
}
@Override
public void mouseExited(final MouseEvent e) {
if (SwingTools.isMouseEventExitedToChildComponents(NotificationPopup.this, e)) {
// not really exited, only moved mouse to child component
return;
}
fadeOutTimer.restart();
}
};
if (isOpacitySupported) {
NotificationPopup.this.setOpacity(alpha);
}
setLayout(new BorderLayout());
add(content, BorderLayout.CENTER);
pack();
}
/**
* Return the {@link MouseListener} which allows closing of the popup when clicking on it.
*
* @return
*/
private MouseListener getMouseListener() {
return mouseListener;
}
@Override
public void setVisible(final boolean b) {
// start fade timer when setting to visible
if (b && !fadeInTimer.isRunning()) {
fadeInTimer.start();
}
super.setVisible(b);
}
@Override
public void dispose() {
if (listener != null) {
listener.popupClosed(NotificationPopup.this);
listener = null;
}
super.dispose();
}
/**
* Shows a fading popup panel which displays the given {@link JPanel} content on the parent
* {@link JComponent} in the specified {@link PopupLocation}. The popup is shown immediately and
* remains visible for the default delay. It can be closed by clicking on it or waiting the
* default delay. If the parent component is not showing, the popup is not shown either.
*
* @param content
* the panel containing what should be shown in the popup
* @param invoker
* the parent component on which the popup should be shown
* @param location
* the location where on the parent component the popup should be shown
* @return the popup or <code>null</code> if it is not showing
*/
public static NotificationPopup showFadingPopup(final JPanel content, final Component invoker,
final PopupLocation location) {
return showFadingPopup(content, invoker, location, DEFAULT_DELAY);
}
/**
* Shows a fading popup panel which displays the given {@link JPanel} content on the parent
* {@link JComponent} in the specified {@link PopupLocation}. The popup is shown immediately and
* remains visible for the specified delay. It can be closed by clicking on it or waiting the
* specified delay. If the parent component is not showing, the popup is not shown either.
*
* @param content
* the panel containing what should be shown in the popup
* @param invoker
* the parent component on which the popup should be shown
* @param location
* the location where on the parent component the popup should be shown
* @param delay
* the delay in milliseconds before the popup starts to fade out
* @return the popup or <code>null</code> if it is not showing
*/
public static NotificationPopup showFadingPopup(final JPanel content, final Component invoker,
final PopupLocation location, final int delay) {
return showFadingPopup(content, invoker, location, delay, 0, 0);
}
/**
* Shows a fading popup panel which displays the given {@link JPanel} content on the parent
* {@link JComponent} in the specified {@link PopupLocation}. The popup is shown immediately and
* remains visible for the specified delay. It can be closed by clicking on it or waiting the
* specified delay. If the parent component is not showing, the popup is not shown either.
*
* @param content
* the panel containing what should be shown in the popup
* @param invoker
* the parent component on which the popup should be shown
* @param location
* the location where on the parent component the popup should be shown
* @param delay
* the delay in milliseconds before the popup starts to fade out
* @param paddingX
* the distance in pixels from the horizontal parent component side
* @param paddingY
* the distance in pixels from the vertical parent component side
* @return the popup or <code>null</code> if it is not showing
*/
public static NotificationPopup showFadingPopup(final JPanel content, final Component invoker,
final PopupLocation location, final int delay, final int paddingX, final int paddingY) {
return showFadingPopup(content, invoker, location, delay, paddingX, paddingY,
BorderFactory.createLineBorder(Color.BLACK, 1, false));
}
/**
* Shows a fading popup panel which displays the given {@link JPanel} content on the parent
* {@link JComponent} in the specified {@link PopupLocation}. The popup is shown immediately and
* remains visible for the specified delay. It can be closed by clicking on it or waiting the
* specified delay. If the parent component is not showing, the popup is not shown either.
*
* @param content
* the panel containing what should be shown in the popup
* @param invoker
* the parent component on which the popup should be shown
* @param location
* the location where on the parent component the popup should be shown
* @param delay
* the delay in milliseconds before the popup starts to fade out
* @param paddingX
* the distance in pixels from the horizontal parent component side
* @param paddingY
* the distance in pixels from the vertical parent component side
* @param border
* the border to display around the notification
* @return the popup or <code>null</code> if it is not showing
*/
public static NotificationPopup showFadingPopup(final JPanel content, final Component invoker,
final PopupLocation location, final int delay, final int paddingX, final int paddingY, final Border border) {
return showFadingPopup(content, invoker, location, delay, paddingX, paddingY,
BorderFactory.createLineBorder(Color.BLACK, 1, false), null);
}
/**
* Shows a fading popup panel which displays the given {@link JPanel} content on the parent
* {@link JComponent} in the specified {@link PopupLocation}. The popup is shown immediately and
* remains visible for the specified delay. It can be closed by clicking on it or waiting the
* specified delay. If the parent component is not showing, the popup is not shown either.
*
* @param content
* the panel containing what should be shown in the popup
* @param invoker
* the parent component on which the popup should be shown
* @param location
* the location where on the parent component the popup should be shown
* @param delay
* the delay in milliseconds before the popup starts to fade out
* @param paddingX
* the distance in pixels from the horizontal parent component side
* @param paddingY
* the distance in pixels from the vertical parent component side
* @param border
* the border to display around the notification
* @param listener
* listener which is notified on events
* @return the popup or <code>null</code> if it is not showing
*/
public static NotificationPopup showFadingPopup(final JPanel content, final Component invoker,
final PopupLocation location, final int delay, final int paddingX, final int paddingY, final Border border,
final NotificationPopupListener listener) {
if (content == null) {
throw new IllegalArgumentException("content must not be null!");
}
if (invoker == null) {
throw new IllegalArgumentException("invoker must not be null!");
}
if (!invoker.isShowing()) {
return null;
}
// if we are in a scrollpane somewhere, we need to actually display on the scrollpane
// otherwise the placement can be off completely or even outside of the screen
Component actualInvoker = invoker;
Container scrollpane = SwingUtilities.getAncestorOfClass(JScrollPane.class, invoker);
if (scrollpane != null) {
actualInvoker = scrollpane;
}
// border first because #pack() is called afterwards
content.setBorder(border);
final NotificationPopup popup = new NotificationPopup(content, delay, listener);
content.addMouseListener(popup.getMouseListener());
int x, y;
switch (location) {
case UPPER_LEFT:
x = 0 + paddingX;
y = 0 + paddingY;
break;
case UPPER_CENTER:
x = (actualInvoker.getWidth() - popup.getPreferredSize().width) / 2 + paddingX;
y = 0 + paddingY;
break;
case UPPER_RIGHT:
x = actualInvoker.getWidth() - popup.getPreferredSize().width - paddingX;
y = 0 + paddingY;
break;
case LOWER_LEFT:
x = 0 + paddingX;
y = actualInvoker.getHeight() - popup.getPreferredSize().height - paddingY;
break;
case LOWER_CENTER:
x = (actualInvoker.getWidth() - popup.getPreferredSize().width) / 2 + paddingX;
y = actualInvoker.getHeight() - popup.getPreferredSize().height - paddingY;
break;
case LOWER_RIGHT:
x = actualInvoker.getWidth() - popup.getPreferredSize().width - paddingX;
y = actualInvoker.getHeight() - popup.getPreferredSize().height - paddingY;
break;
case CENTER:
x = (actualInvoker.getWidth() - popup.getPreferredSize().width) / 2 + paddingX;
y = (actualInvoker.getHeight() - popup.getPreferredSize().height) / 2 + paddingY;
break;
case CENTER_LEFT:
x = 0 + paddingX;
y = (actualInvoker.getHeight() - popup.getPreferredSize().height) / 2 + paddingY;
break;
case CENTER_RIGHT:
x = actualInvoker.getWidth() - popup.getPreferredSize().width - paddingX;
y = (actualInvoker.getHeight() - popup.getPreferredSize().height) / 2 + paddingY;
break;
default:
x = 0 + paddingX;
y = 0 + paddingY;
}
x += actualInvoker.getLocationOnScreen().getX();
y += actualInvoker.getLocationOnScreen().getY();
popup.setLocation(x, y);
popup.setFocusableWindowState(false); // does not take focus away from the currently focused
// component
popup.setVisible(true);
popup.setFocusableWindowState(true); // afterwards it should be focusable
return popup;
}
/**
* Restarts the timer until fadeout if notification has not started to fade out.
*
* @return {@code true} if the restart was successful
*/
public boolean restartTimer() {
if (fadeInTimer.isRunning()) {
// fadeOutTimer not started yet
return true;
}
if (fadeOutTimer.isRunning() && alpha >= 1.0f) {
fadeOutTimer.restart();
return true;
}
return false;
}
}