001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.awt.BasicStroke;
007import java.awt.Color;
008import java.awt.Component;
009import java.awt.Container;
010import java.awt.Dimension;
011import java.awt.Graphics;
012import java.awt.Graphics2D;
013import java.awt.Insets;
014import java.awt.Point;
015import java.awt.RenderingHints;
016import java.awt.Shape;
017import java.awt.event.ActionEvent;
018import java.awt.event.ActionListener;
019import java.awt.event.MouseAdapter;
020import java.awt.event.MouseEvent;
021import java.awt.event.MouseListener;
022import java.awt.geom.RoundRectangle2D;
023import java.util.LinkedList;
024import java.util.Queue;
025
026import javax.swing.AbstractAction;
027import javax.swing.BorderFactory;
028import javax.swing.GroupLayout;
029import javax.swing.JButton;
030import javax.swing.JFrame;
031import javax.swing.JLabel;
032import javax.swing.JLayeredPane;
033import javax.swing.JPanel;
034import javax.swing.JToolBar;
035import javax.swing.SwingUtilities;
036import javax.swing.Timer;
037
038import org.openstreetmap.josm.Main;
039import org.openstreetmap.josm.gui.help.HelpBrowser;
040import org.openstreetmap.josm.gui.help.HelpUtil;
041import org.openstreetmap.josm.tools.ImageProvider;
042
043/**
044 * Manages {@link Notification}s, i.e. displays them on screen.
045 *
046 * Don't use this class directly, but use {@link Notification#show()}.
047 *
048 * If multiple messages are sent in a short period of time, they are put in
049 * a queue and displayed one after the other.
050 *
051 * The user can stop the timer (freeze the message) by moving the mouse cursor
052 * above the panel. As a visual cue, the background color changes from
053 * semi-transparent to opaque while the timer is frozen.
054 */
055class NotificationManager {
056
057    private final Timer hideTimer; // started when message is shown, responsible for hiding the message
058    private final Timer pauseTimer; // makes sure, there is a small pause between two consecutive messages
059    private final Timer unfreezeDelayTimer; // tiny delay before resuming the timer when mouse cursor is moved off the panel
060    private boolean running;
061
062    private Notification currentNotification;
063    private NotificationPanel currentNotificationPanel;
064    private final Queue<Notification> queue;
065
066    private static int pauseTime = Main.pref.getInteger("notification-default-pause-time-ms", 300); // milliseconds
067    static int defaultNotificationTime = Main.pref.getInteger("notification-default-time-ms", 5000); // milliseconds
068
069    private long displayTimeStart;
070    private long elapsedTime;
071
072    private static NotificationManager INSTANCE;
073
074    private static final Color PANEL_SEMITRANSPARENT = new Color(224, 236, 249, 230);
075    private static final Color PANEL_OPAQUE = new Color(224, 236, 249);
076
077    public static synchronized NotificationManager getInstance() {
078        if (INSTANCE == null) {
079            INSTANCE = new NotificationManager();
080        }
081        return INSTANCE;
082    }
083
084    NotificationManager() {
085        queue = new LinkedList<>();
086        hideTimer = new Timer(defaultNotificationTime, new HideEvent());
087        hideTimer.setRepeats(false);
088        pauseTimer = new Timer(pauseTime, new PauseFinishedEvent());
089        pauseTimer.setRepeats(false);
090        unfreezeDelayTimer = new Timer(10, new UnfreezeEvent());
091        unfreezeDelayTimer.setRepeats(false);
092    }
093
094    public void showNotification(Notification note) {
095        synchronized (queue) {
096            queue.add(note);
097            processQueue();
098        }
099    }
100
101    private void processQueue() {
102        if (running) return;
103
104        currentNotification = queue.poll();
105        if (currentNotification == null) return;
106
107        currentNotificationPanel = new NotificationPanel(currentNotification);
108        currentNotificationPanel.validate();
109
110        int MARGIN = 5;
111        int x, y;
112        JFrame parentWindow = (JFrame) Main.parent;
113        Dimension size = currentNotificationPanel.getPreferredSize();
114        if (Main.isDisplayingMapView() && Main.map.mapView.getHeight() > 0) {
115            MapView mv = Main.map.mapView;
116            Point mapViewPos = SwingUtilities.convertPoint(mv.getParent(), mv.getX(), mv.getY(), Main.parent);
117            x = mapViewPos.x + MARGIN;
118            y = mapViewPos.y + mv.getHeight() - Main.map.statusLine.getHeight() - size.height - MARGIN;
119        } else {
120            x = MARGIN;
121            y = parentWindow.getHeight() - Main.toolbar.control.getSize().height - size.height - MARGIN;
122        }
123        parentWindow.getLayeredPane().add(currentNotificationPanel, JLayeredPane.POPUP_LAYER, 0);
124
125        currentNotificationPanel.setLocation(x, y);
126        currentNotificationPanel.setSize(size);
127
128        currentNotificationPanel.setVisible(true);
129
130        running = true;
131        elapsedTime = 0;
132
133        startHideTimer();
134    }
135
136    private void startHideTimer() {
137        int remaining = (int) (currentNotification.getDuration() - elapsedTime);
138        if (remaining < 300) {
139            remaining = 300;
140        }
141        displayTimeStart = System.currentTimeMillis();
142        hideTimer.setInitialDelay(remaining);
143        hideTimer.restart();
144    }
145
146    private class HideEvent implements ActionListener {
147
148        @Override
149        public void actionPerformed(ActionEvent e) {
150            hideTimer.stop();
151            if (currentNotificationPanel != null) {
152                currentNotificationPanel.setVisible(false);
153                ((JFrame) Main.parent).getLayeredPane().remove(currentNotificationPanel);
154                currentNotificationPanel = null;
155            }
156            pauseTimer.restart();
157        }
158    }
159
160    private class PauseFinishedEvent implements ActionListener {
161
162        @Override
163        public void actionPerformed(ActionEvent e) {
164            synchronized (queue) {
165                running = false;
166                processQueue();
167            }
168        }
169    }
170
171    private class UnfreezeEvent implements ActionListener {
172
173        @Override
174        public void actionPerformed(ActionEvent e) {
175            if (currentNotificationPanel != null) {
176                currentNotificationPanel.setNotificationBackground(PANEL_SEMITRANSPARENT);
177                currentNotificationPanel.repaint();
178            }
179            startHideTimer();
180        }
181    }
182
183    private class NotificationPanel extends JPanel {
184
185        private JPanel innerPanel;
186
187        NotificationPanel(Notification note) {
188            setVisible(false);
189            build(note);
190        }
191
192        public void setNotificationBackground(Color c) {
193            innerPanel.setBackground(c);
194        }
195
196        private void build(final Notification note) {
197            JButton btnClose = new JButton(new HideAction());
198            btnClose.setPreferredSize(new Dimension(50, 50));
199            btnClose.setMargin(new Insets(0, 0, 1, 1));
200            btnClose.setContentAreaFilled(false);
201            // put it in JToolBar to get a better appearance
202            JToolBar tbClose = new JToolBar();
203            tbClose.setFloatable(false);
204            tbClose.setBorderPainted(false);
205            tbClose.setOpaque(false);
206            tbClose.add(btnClose);
207
208            JToolBar tbHelp = null;
209            if (note.getHelpTopic() != null) {
210                JButton btnHelp = new JButton(tr("Help"));
211                btnHelp.setIcon(ImageProvider.get("help"));
212                btnHelp.setToolTipText(tr("Show help information"));
213                HelpUtil.setHelpContext(btnHelp, note.getHelpTopic());
214                btnHelp.addActionListener(new AbstractAction() {
215                    @Override
216                    public void actionPerformed(ActionEvent e) {
217                        SwingUtilities.invokeLater(new Runnable() {
218                            @Override
219                            public void run() {
220                                HelpBrowser.setUrlForHelpTopic(note.getHelpTopic());
221                            }
222                        });
223                    }
224                });
225                btnHelp.setOpaque(false);
226                tbHelp = new JToolBar();
227                tbHelp.setFloatable(false);
228                tbHelp.setBorderPainted(false);
229                tbHelp.setOpaque(false);
230                tbHelp.add(btnHelp);
231            }
232
233            setOpaque(false);
234            innerPanel = new RoundedPanel();
235            innerPanel.setBackground(PANEL_SEMITRANSPARENT);
236            innerPanel.setForeground(Color.BLACK);
237
238            GroupLayout layout = new GroupLayout(innerPanel);
239            innerPanel.setLayout(layout);
240            layout.setAutoCreateGaps(true);
241            layout.setAutoCreateContainerGaps(true);
242
243            innerPanel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
244            add(innerPanel);
245
246            JLabel icon = null;
247            if (note.getIcon() != null) {
248                icon = new JLabel(note.getIcon());
249            }
250            Component content = note.getContent();
251            GroupLayout.SequentialGroup hgroup = layout.createSequentialGroup();
252            if (icon != null) {
253                hgroup.addComponent(icon);
254            }
255            if (tbHelp != null) {
256                hgroup.addGroup(layout.createParallelGroup(GroupLayout.Alignment.TRAILING)
257                        .addComponent(content)
258                        .addComponent(tbHelp)
259                );
260            } else {
261                hgroup.addComponent(content);
262            }
263            hgroup.addComponent(tbClose);
264            GroupLayout.ParallelGroup vgroup = layout.createParallelGroup();
265            if (icon != null) {
266                vgroup.addComponent(icon);
267            }
268            vgroup.addComponent(content);
269            vgroup.addComponent(tbClose);
270            layout.setHorizontalGroup(hgroup);
271
272            if (tbHelp != null) {
273                layout.setVerticalGroup(layout.createSequentialGroup()
274                        .addGroup(vgroup)
275                        .addComponent(tbHelp)
276                );
277            } else {
278                layout.setVerticalGroup(vgroup);
279            }
280
281            /*
282             * The timer stops when the mouse cursor is above the panel.
283             *
284             * This is not straightforward, because the JPanel will get a
285             * mouseExited event when the cursor moves on top of the JButton
286             * inside the panel.
287             *
288             * The current hacky solution is to register the freeze MouseListener
289             * not only to the panel, but to all the components inside the panel.
290             *
291             * Moving the mouse cursor from one component to the next would
292             * cause some flickering (timer is started and stopped for a fraction
293             * of a second, background color is switched twice), so there is
294             * a tiny delay before the timer really resumes.
295             */
296            MouseListener freeze = new FreezeMouseListener();
297            addMouseListenerToAllChildComponents(this, freeze);
298        }
299
300        private void addMouseListenerToAllChildComponents(Component comp, MouseListener listener) {
301            comp.addMouseListener(listener);
302            if (comp instanceof Container) {
303                for (Component c: ((Container) comp).getComponents()) {
304                    addMouseListenerToAllChildComponents(c, listener);
305                }
306            }
307        }
308
309        class HideAction extends AbstractAction {
310
311            HideAction() {
312                putValue(SMALL_ICON, ImageProvider.get("misc", "grey_x"));
313            }
314
315            @Override
316            public void actionPerformed(ActionEvent e) {
317                new HideEvent().actionPerformed(null);
318            }
319        }
320
321        class FreezeMouseListener extends MouseAdapter {
322            @Override
323            public void mouseEntered(MouseEvent e) {
324                if (unfreezeDelayTimer.isRunning()) {
325                    unfreezeDelayTimer.stop();
326                } else {
327                    hideTimer.stop();
328                    elapsedTime += System.currentTimeMillis() - displayTimeStart;
329                    currentNotificationPanel.setNotificationBackground(PANEL_OPAQUE);
330                    currentNotificationPanel.repaint();
331                }
332            }
333
334            @Override
335            public void mouseExited(MouseEvent e) {
336                unfreezeDelayTimer.restart();
337            }
338        }
339    }
340
341    /**
342     * A panel with rounded edges and line border.
343     */
344    public static class RoundedPanel extends JPanel {
345
346        RoundedPanel() {
347            super();
348            setOpaque(false);
349        }
350
351        @Override
352        protected void paintComponent(Graphics graphics) {
353            Graphics2D g = (Graphics2D) graphics;
354            g.setRenderingHint(
355                    RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
356            g.setColor(getBackground());
357            float lineWidth = 1.4f;
358            Shape rect = new RoundRectangle2D.Double(
359                    lineWidth/2d + getInsets().left,
360                    lineWidth/2d + getInsets().top,
361                    getWidth() - lineWidth/2d - getInsets().left - getInsets().right,
362                    getHeight() - lineWidth/2d - getInsets().top - getInsets().bottom,
363                    20, 20);
364
365            g.fill(rect);
366            g.setColor(getForeground());
367            g.setStroke(new BasicStroke(lineWidth));
368            g.draw(rect);
369            super.paintComponent(graphics);
370        }
371    }
372}