// 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.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 Timer hideTimer; // started when message is shown, responsible for hiding the message private Timer pauseTimer; // makes sure, there is a small pause between two consecutive messages private 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 queue; private static int pauseTime = Main.pref.getInteger("notification-default-pause-time-ms", 300); // milliseconds static int defaultNotificationTime = Main.pref.getInteger("notification-default-time-ms", 5000); // milliseconds private long displayTimeStart; private long elapsedTime; private static NotificationManager INSTANCE = null; private final Color PANEL_SEMITRANSPARENT = new Color(224, 236, 249, 230); private final Color PANEL_OPAQUE = new Color(224, 236, 249); public static NotificationManager getInstance() { if (INSTANCE == null) { INSTANCE = new NotificationManager(); } return INSTANCE; } public NotificationManager() { queue = new LinkedList<>(); hideTimer = new Timer(defaultNotificationTime, new HideEvent()); hideTimer.setRepeats(false); pauseTimer = new Timer(pauseTime, new PauseFinishedEvent()); pauseTimer.setRepeats(false); unfreezeDelayTimer = new Timer(10, new UnfreezeEvent()); unfreezeDelayTimer.setRepeats(false); } 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); currentNotificationPanel.validate(); int MARGIN = 5; int x, y; JFrame parentWindow = (JFrame) Main.parent; Dimension size = currentNotificationPanel.getPreferredSize(); 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 class HideEvent implements ActionListener { @Override public void actionPerformed(ActionEvent e) { hideTimer.stop(); currentNotificationPanel.setVisible(false); ((JFrame) Main.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) { currentNotificationPanel.setNotificationBackground(PANEL_SEMITRANSPARENT); currentNotificationPanel.repaint(); startHideTimer(); } } private class NotificationPanel extends JPanel { private JPanel innerPanel; public NotificationPanel(Notification note) { setVisible(false); build(note); } public void setNotificationBackground(Color c) { innerPanel.setBackground(c); } private void build(final Notification note) { JButton btnClose = new JButton(new HideAction()); 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 AbstractAction() { @Override public void actionPerformed(ActionEvent e) { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { HelpBrowser.setUrlForHelpTopic(note.getHelpTopic()); } }); } }); 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. */ MouseListener freeze = new FreezeMouseListener(); addMouseListenerToAllChildComponents(this, freeze); } private void addMouseListenerToAllChildComponents(Component comp, MouseListener listener) { comp.addMouseListener(listener); if (comp instanceof Container) { for (Component c: ((Container)comp).getComponents()) { addMouseListenerToAllChildComponents(c, listener); } } } class HideAction extends AbstractAction { public HideAction() { putValue(SMALL_ICON, ImageProvider.get("misc", "grey_x")); } @Override public void actionPerformed(ActionEvent e) { new HideEvent().actionPerformed(null); } } 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 { public 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.Float( lineWidth/2 + getInsets().left, lineWidth/2 + getInsets().top, getWidth() - lineWidth/2 - getInsets().left - getInsets().right, getHeight() - lineWidth/2 - getInsets().top - getInsets().bottom, 20, 20); g.fill(rect); g.setColor(getForeground()); g.setStroke(new BasicStroke(lineWidth)); g.draw(rect); super.paintComponent(graphics); } } }