// License: GPL. For details, see LICENSE file.
package org.openstreetmap.josm.gui;
import static org.openstreetmap.josm.tools.I18n.tr;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Component;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Insets;
import java.awt.Point;
import java.awt.RenderingHints;
import java.awt.Shape;
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 java.awt.geom.RoundRectangle2D;
import java.util.LinkedList;
import java.util.Queue;
import javax.swing.AbstractAction;
import javax.swing.BorderFactory;
import javax.swing.GroupLayout;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JLayeredPane;
import javax.swing.JPanel;
import javax.swing.JToolBar;
import javax.swing.SwingUtilities;
import javax.swing.Timer;
import org.openstreetmap.josm.Main;
import org.openstreetmap.josm.data.preferences.IntegerProperty;
import org.openstreetmap.josm.gui.help.HelpBrowser;
import org.openstreetmap.josm.gui.help.HelpUtil;
import org.openstreetmap.josm.tools.ImageProvider;
/**
* Manages {@link Notification}s, i.e. displays them on screen.
*
* Don't use this class directly, but use {@link Notification#show()}.
*
* If multiple messages are sent in a short period of time, they are put in
* a queue and displayed one after the other.
*
* The user can stop the timer (freeze the message) by moving the mouse cursor
* above the panel. As a visual cue, the background color changes from
* semi-transparent to opaque while the timer is frozen.
*/
class NotificationManager {
private final Timer hideTimer; // started when message is shown, responsible for hiding the message
private final Timer pauseTimer; // makes sure, there is a small pause between two consecutive messages
private final Timer unfreezeDelayTimer; // tiny delay before resuming the timer when mouse cursor is moved off the panel
private boolean running;
private Notification currentNotification;
private NotificationPanel currentNotificationPanel;
private final Queue<Notification> queue;
private static IntegerProperty pauseTime = new IntegerProperty("notification-default-pause-time-ms", 300); // milliseconds
private long displayTimeStart;
private long elapsedTime;
private static NotificationManager instance;
private static final Color PANEL_SEMITRANSPARENT = new Color(224, 236, 249, 230);
private static final Color PANEL_OPAQUE = new Color(224, 236, 249);
NotificationManager() {
queue = new LinkedList<>();
hideTimer = new Timer(Notification.TIME_DEFAULT, e -> this.stopHideTimer());
hideTimer.setRepeats(false);
pauseTimer = new Timer(pauseTime.get(), new PauseFinishedEvent());
pauseTimer.setRepeats(false);
unfreezeDelayTimer = new Timer(10, new UnfreezeEvent());
unfreezeDelayTimer.setRepeats(false);
}
/**
* Show the given notification
* @param note The note to show.
* @see Notification#show()
*/
public void showNotification(Notification note) {
synchronized (queue) {
queue.add(note);
processQueue();
}
}
private void processQueue() {
if (running) return;
currentNotification = queue.poll();
if (currentNotification == null) return;
currentNotificationPanel = new NotificationPanel(currentNotification, new FreezeMouseListener(), e -> this.stopHideTimer());
currentNotificationPanel.validate();
int margin = 5;
JFrame parentWindow = (JFrame) Main.parent;
Dimension size = currentNotificationPanel.getPreferredSize();
if (parentWindow != null) {
int x;
int y;
if (Main.isDisplayingMapView() && Main.map.mapView.getHeight() > 0) {
MapView mv = Main.map.mapView;
Point mapViewPos = SwingUtilities.convertPoint(mv.getParent(), mv.getX(), mv.getY(), Main.parent);
x = mapViewPos.x + margin;
y = mapViewPos.y + mv.getHeight() - Main.map.statusLine.getHeight() - size.height - margin;
} else {
x = margin;
y = parentWindow.getHeight() - Main.toolbar.control.getSize().height - size.height - margin;
}
parentWindow.getLayeredPane().add(currentNotificationPanel, JLayeredPane.POPUP_LAYER, 0);
currentNotificationPanel.setLocation(x, y);
}
currentNotificationPanel.setSize(size);
currentNotificationPanel.setVisible(true);
running = true;
elapsedTime = 0;
startHideTimer();
}
private void startHideTimer() {
int remaining = (int) (currentNotification.getDuration() - elapsedTime);
if (remaining < 300) {
remaining = 300;
}
displayTimeStart = System.currentTimeMillis();
hideTimer.setInitialDelay(remaining);
hideTimer.restart();
}
private void stopHideTimer() {
hideTimer.stop();
if (currentNotificationPanel != null) {
currentNotificationPanel.setVisible(false);
JFrame parent = (JFrame) Main.parent;
if (parent != null) {
parent.getLayeredPane().remove(currentNotificationPanel);
}
currentNotificationPanel = null;
}
pauseTimer.restart();
}
private class PauseFinishedEvent implements ActionListener {
@Override
public void actionPerformed(ActionEvent e) {
synchronized (queue) {
running = false;
processQueue();
}
}
}
private class UnfreezeEvent implements ActionListener {
@Override
public void actionPerformed(ActionEvent e) {
if (currentNotificationPanel != null) {
currentNotificationPanel.setNotificationBackground(PANEL_SEMITRANSPARENT);
currentNotificationPanel.repaint();
}
startHideTimer();
}
}
private static class NotificationPanel extends JPanel {
static final class ShowNoteHelpAction extends AbstractAction {
private final Notification note;
ShowNoteHelpAction(Notification note) {
this.note = note;
}
@Override
public void actionPerformed(ActionEvent e) {
SwingUtilities.invokeLater(() -> HelpBrowser.setUrlForHelpTopic(note.getHelpTopic()));
}
}
private JPanel innerPanel;
NotificationPanel(Notification note, MouseListener freeze, ActionListener hideListener) {
setVisible(false);
build(note, freeze, hideListener);
}
public void setNotificationBackground(Color c) {
innerPanel.setBackground(c);
}
private void build(final Notification note, MouseListener freeze, ActionListener hideListener) {
JButton btnClose = new JButton();
btnClose.addActionListener(hideListener);
btnClose.setIcon(ImageProvider.get("misc", "grey_x"));
btnClose.setPreferredSize(new Dimension(50, 50));
btnClose.setMargin(new Insets(0, 0, 1, 1));
btnClose.setContentAreaFilled(false);
// put it in JToolBar to get a better appearance
JToolBar tbClose = new JToolBar();
tbClose.setFloatable(false);
tbClose.setBorderPainted(false);
tbClose.setOpaque(false);
tbClose.add(btnClose);
JToolBar tbHelp = null;
if (note.getHelpTopic() != null) {
JButton btnHelp = new JButton(tr("Help"));
btnHelp.setIcon(ImageProvider.get("help"));
btnHelp.setToolTipText(tr("Show help information"));
HelpUtil.setHelpContext(btnHelp, note.getHelpTopic());
btnHelp.addActionListener(new ShowNoteHelpAction(note));
btnHelp.setOpaque(false);
tbHelp = new JToolBar();
tbHelp.setFloatable(false);
tbHelp.setBorderPainted(false);
tbHelp.setOpaque(false);
tbHelp.add(btnHelp);
}
setOpaque(false);
innerPanel = new RoundedPanel();
innerPanel.setBackground(PANEL_SEMITRANSPARENT);
innerPanel.setForeground(Color.BLACK);
GroupLayout layout = new GroupLayout(innerPanel);
innerPanel.setLayout(layout);
layout.setAutoCreateGaps(true);
layout.setAutoCreateContainerGaps(true);
innerPanel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
add(innerPanel);
JLabel icon = null;
if (note.getIcon() != null) {
icon = new JLabel(note.getIcon());
}
Component content = note.getContent();
GroupLayout.SequentialGroup hgroup = layout.createSequentialGroup();
if (icon != null) {
hgroup.addComponent(icon);
}
if (tbHelp != null) {
hgroup.addGroup(layout.createParallelGroup(GroupLayout.Alignment.TRAILING)
.addComponent(content)
.addComponent(tbHelp)
);
} else {
hgroup.addComponent(content);
}
hgroup.addComponent(tbClose);
GroupLayout.ParallelGroup vgroup = layout.createParallelGroup();
if (icon != null) {
vgroup.addComponent(icon);
}
vgroup.addComponent(content);
vgroup.addComponent(tbClose);
layout.setHorizontalGroup(hgroup);
if (tbHelp != null) {
layout.setVerticalGroup(layout.createSequentialGroup()
.addGroup(vgroup)
.addComponent(tbHelp)
);
} else {
layout.setVerticalGroup(vgroup);
}
/*
* The timer stops when the mouse cursor is above the panel.
*
* This is not straightforward, because the JPanel will get a
* mouseExited event when the cursor moves on top of the JButton
* inside the panel.
*
* The current hacky solution is to register the freeze MouseListener
* not only to the panel, but to all the components inside the panel.
*
* Moving the mouse cursor from one component to the next would
* cause some flickering (timer is started and stopped for a fraction
* of a second, background color is switched twice), so there is
* a tiny delay before the timer really resumes.
*/
addMouseListenerToAllChildComponents(this, freeze);
}
private static void addMouseListenerToAllChildComponents(Component comp, MouseListener listener) {
comp.addMouseListener(listener);
if (comp instanceof Container) {
for (Component c: ((Container) comp).getComponents()) {
addMouseListenerToAllChildComponents(c, listener);
}
}
}
}
class FreezeMouseListener extends MouseAdapter {
@Override
public void mouseEntered(MouseEvent e) {
if (unfreezeDelayTimer.isRunning()) {
unfreezeDelayTimer.stop();
} else {
hideTimer.stop();
elapsedTime += System.currentTimeMillis() - displayTimeStart;
currentNotificationPanel.setNotificationBackground(PANEL_OPAQUE);
currentNotificationPanel.repaint();
}
}
@Override
public void mouseExited(MouseEvent e) {
unfreezeDelayTimer.restart();
}
}
/**
* A panel with rounded edges and line border.
*/
public static class RoundedPanel extends JPanel {
RoundedPanel() {
super();
setOpaque(false);
}
@Override
protected void paintComponent(Graphics graphics) {
Graphics2D g = (Graphics2D) graphics;
g.setRenderingHint(
RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g.setColor(getBackground());
float lineWidth = 1.4f;
Shape rect = new RoundRectangle2D.Double(
lineWidth/2d + getInsets().left,
lineWidth/2d + getInsets().top,
getWidth() - lineWidth/2d - getInsets().left - getInsets().right,
getHeight() - lineWidth/2d - getInsets().top - getInsets().bottom,
20, 20);
g.fill(rect);
g.setColor(getForeground());
g.setStroke(new BasicStroke(lineWidth));
g.draw(rect);
super.paintComponent(graphics);
}
}
public static synchronized NotificationManager getInstance() {
if (instance == null) {
instance = new NotificationManager();
}
return instance;
}
}