source: josm/trunk/src/org/openstreetmap/josm/gui/NotificationManager.java@ 10304

Last change on this file since 10304 was 10256, checked in by Don-vip, 8 years ago

add more unit tests

  • Property svn:eol-style set to native
File size: 13.3 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.gui;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5
6import java.awt.BasicStroke;
7import java.awt.Color;
8import java.awt.Component;
9import java.awt.Container;
10import java.awt.Dimension;
11import java.awt.Graphics;
12import java.awt.Graphics2D;
13import java.awt.Insets;
14import java.awt.Point;
15import java.awt.RenderingHints;
16import java.awt.Shape;
17import java.awt.event.ActionEvent;
18import java.awt.event.ActionListener;
19import java.awt.event.MouseAdapter;
20import java.awt.event.MouseEvent;
21import java.awt.event.MouseListener;
22import java.awt.geom.RoundRectangle2D;
23import java.util.LinkedList;
24import java.util.Queue;
25
26import javax.swing.AbstractAction;
27import javax.swing.BorderFactory;
28import javax.swing.GroupLayout;
29import javax.swing.JButton;
30import javax.swing.JFrame;
31import javax.swing.JLabel;
32import javax.swing.JLayeredPane;
33import javax.swing.JPanel;
34import javax.swing.JToolBar;
35import javax.swing.SwingUtilities;
36import javax.swing.Timer;
37
38import org.openstreetmap.josm.Main;
39import org.openstreetmap.josm.gui.help.HelpBrowser;
40import org.openstreetmap.josm.gui.help.HelpUtil;
41import org.openstreetmap.josm.tools.ImageProvider;
42
43/**
44 * Manages {@link Notification}s, i.e. displays them on screen.
45 *
46 * Don't use this class directly, but use {@link Notification#show()}.
47 *
48 * If multiple messages are sent in a short period of time, they are put in
49 * a queue and displayed one after the other.
50 *
51 * The user can stop the timer (freeze the message) by moving the mouse cursor
52 * above the panel. As a visual cue, the background color changes from
53 * semi-transparent to opaque while the timer is frozen.
54 */
55class NotificationManager {
56
57 private final Timer hideTimer; // started when message is shown, responsible for hiding the message
58 private final Timer pauseTimer; // makes sure, there is a small pause between two consecutive messages
59 private final Timer unfreezeDelayTimer; // tiny delay before resuming the timer when mouse cursor is moved off the panel
60 private boolean running;
61
62 private Notification currentNotification;
63 private NotificationPanel currentNotificationPanel;
64 private final Queue<Notification> queue;
65
66 private static int pauseTime = Main.pref.getInteger("notification-default-pause-time-ms", 300); // milliseconds
67 static int defaultNotificationTime = Main.pref.getInteger("notification-default-time-ms", 5000); // milliseconds
68
69 private long displayTimeStart;
70 private long elapsedTime;
71
72 private static NotificationManager INSTANCE;
73
74 private static final Color PANEL_SEMITRANSPARENT = new Color(224, 236, 249, 230);
75 private static final Color PANEL_OPAQUE = new Color(224, 236, 249);
76
77 public static synchronized NotificationManager getInstance() {
78 if (INSTANCE == null) {
79 INSTANCE = new NotificationManager();
80 }
81 return INSTANCE;
82 }
83
84 NotificationManager() {
85 queue = new LinkedList<>();
86 hideTimer = new Timer(defaultNotificationTime, new HideEvent());
87 hideTimer.setRepeats(false);
88 pauseTimer = new Timer(pauseTime, new PauseFinishedEvent());
89 pauseTimer.setRepeats(false);
90 unfreezeDelayTimer = new Timer(10, new UnfreezeEvent());
91 unfreezeDelayTimer.setRepeats(false);
92 }
93
94 public void showNotification(Notification note) {
95 synchronized (queue) {
96 queue.add(note);
97 processQueue();
98 }
99 }
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 JFrame parentWindow = (JFrame) Main.parent;
112 Dimension size = currentNotificationPanel.getPreferredSize();
113 if (parentWindow != null) {
114 int x;
115 int y;
116 if (Main.isDisplayingMapView() && Main.map.mapView.getHeight() > 0) {
117 MapView mv = Main.map.mapView;
118 Point mapViewPos = SwingUtilities.convertPoint(mv.getParent(), mv.getX(), mv.getY(), Main.parent);
119 x = mapViewPos.x + margin;
120 y = mapViewPos.y + mv.getHeight() - Main.map.statusLine.getHeight() - size.height - margin;
121 } else {
122 x = margin;
123 y = parentWindow.getHeight() - Main.toolbar.control.getSize().height - size.height - margin;
124 }
125 parentWindow.getLayeredPane().add(currentNotificationPanel, JLayeredPane.POPUP_LAYER, 0);
126
127 currentNotificationPanel.setLocation(x, y);
128 }
129 currentNotificationPanel.setSize(size);
130
131 currentNotificationPanel.setVisible(true);
132
133 running = true;
134 elapsedTime = 0;
135
136 startHideTimer();
137 }
138
139 private void startHideTimer() {
140 int remaining = (int) (currentNotification.getDuration() - elapsedTime);
141 if (remaining < 300) {
142 remaining = 300;
143 }
144 displayTimeStart = System.currentTimeMillis();
145 hideTimer.setInitialDelay(remaining);
146 hideTimer.restart();
147 }
148
149 private class HideEvent implements ActionListener {
150
151 @Override
152 public void actionPerformed(ActionEvent e) {
153 hideTimer.stop();
154 if (currentNotificationPanel != null) {
155 currentNotificationPanel.setVisible(false);
156 ((JFrame) Main.parent).getLayeredPane().remove(currentNotificationPanel);
157 currentNotificationPanel = null;
158 }
159 pauseTimer.restart();
160 }
161 }
162
163 private class PauseFinishedEvent implements ActionListener {
164
165 @Override
166 public void actionPerformed(ActionEvent e) {
167 synchronized (queue) {
168 running = false;
169 processQueue();
170 }
171 }
172 }
173
174 private class UnfreezeEvent implements ActionListener {
175
176 @Override
177 public void actionPerformed(ActionEvent e) {
178 if (currentNotificationPanel != null) {
179 currentNotificationPanel.setNotificationBackground(PANEL_SEMITRANSPARENT);
180 currentNotificationPanel.repaint();
181 }
182 startHideTimer();
183 }
184 }
185
186 private class NotificationPanel extends JPanel {
187
188 private JPanel innerPanel;
189
190 NotificationPanel(Notification note) {
191 setVisible(false);
192 build(note);
193 }
194
195 public void setNotificationBackground(Color c) {
196 innerPanel.setBackground(c);
197 }
198
199 private void build(final Notification note) {
200 JButton btnClose = new JButton(new HideAction());
201 btnClose.setPreferredSize(new Dimension(50, 50));
202 btnClose.setMargin(new Insets(0, 0, 1, 1));
203 btnClose.setContentAreaFilled(false);
204 // put it in JToolBar to get a better appearance
205 JToolBar tbClose = new JToolBar();
206 tbClose.setFloatable(false);
207 tbClose.setBorderPainted(false);
208 tbClose.setOpaque(false);
209 tbClose.add(btnClose);
210
211 JToolBar tbHelp = null;
212 if (note.getHelpTopic() != null) {
213 JButton btnHelp = new JButton(tr("Help"));
214 btnHelp.setIcon(ImageProvider.get("help"));
215 btnHelp.setToolTipText(tr("Show help information"));
216 HelpUtil.setHelpContext(btnHelp, note.getHelpTopic());
217 btnHelp.addActionListener(new AbstractAction() {
218 @Override
219 public void actionPerformed(ActionEvent e) {
220 SwingUtilities.invokeLater(new Runnable() {
221 @Override
222 public void run() {
223 HelpBrowser.setUrlForHelpTopic(note.getHelpTopic());
224 }
225 });
226 }
227 });
228 btnHelp.setOpaque(false);
229 tbHelp = new JToolBar();
230 tbHelp.setFloatable(false);
231 tbHelp.setBorderPainted(false);
232 tbHelp.setOpaque(false);
233 tbHelp.add(btnHelp);
234 }
235
236 setOpaque(false);
237 innerPanel = new RoundedPanel();
238 innerPanel.setBackground(PANEL_SEMITRANSPARENT);
239 innerPanel.setForeground(Color.BLACK);
240
241 GroupLayout layout = new GroupLayout(innerPanel);
242 innerPanel.setLayout(layout);
243 layout.setAutoCreateGaps(true);
244 layout.setAutoCreateContainerGaps(true);
245
246 innerPanel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
247 add(innerPanel);
248
249 JLabel icon = null;
250 if (note.getIcon() != null) {
251 icon = new JLabel(note.getIcon());
252 }
253 Component content = note.getContent();
254 GroupLayout.SequentialGroup hgroup = layout.createSequentialGroup();
255 if (icon != null) {
256 hgroup.addComponent(icon);
257 }
258 if (tbHelp != null) {
259 hgroup.addGroup(layout.createParallelGroup(GroupLayout.Alignment.TRAILING)
260 .addComponent(content)
261 .addComponent(tbHelp)
262 );
263 } else {
264 hgroup.addComponent(content);
265 }
266 hgroup.addComponent(tbClose);
267 GroupLayout.ParallelGroup vgroup = layout.createParallelGroup();
268 if (icon != null) {
269 vgroup.addComponent(icon);
270 }
271 vgroup.addComponent(content);
272 vgroup.addComponent(tbClose);
273 layout.setHorizontalGroup(hgroup);
274
275 if (tbHelp != null) {
276 layout.setVerticalGroup(layout.createSequentialGroup()
277 .addGroup(vgroup)
278 .addComponent(tbHelp)
279 );
280 } else {
281 layout.setVerticalGroup(vgroup);
282 }
283
284 /*
285 * The timer stops when the mouse cursor is above the panel.
286 *
287 * This is not straightforward, because the JPanel will get a
288 * mouseExited event when the cursor moves on top of the JButton
289 * inside the panel.
290 *
291 * The current hacky solution is to register the freeze MouseListener
292 * not only to the panel, but to all the components inside the panel.
293 *
294 * Moving the mouse cursor from one component to the next would
295 * cause some flickering (timer is started and stopped for a fraction
296 * of a second, background color is switched twice), so there is
297 * a tiny delay before the timer really resumes.
298 */
299 MouseListener freeze = new FreezeMouseListener();
300 addMouseListenerToAllChildComponents(this, freeze);
301 }
302
303 private void addMouseListenerToAllChildComponents(Component comp, MouseListener listener) {
304 comp.addMouseListener(listener);
305 if (comp instanceof Container) {
306 for (Component c: ((Container) comp).getComponents()) {
307 addMouseListenerToAllChildComponents(c, listener);
308 }
309 }
310 }
311
312 class HideAction extends AbstractAction {
313
314 HideAction() {
315 putValue(SMALL_ICON, ImageProvider.get("misc", "grey_x"));
316 }
317
318 @Override
319 public void actionPerformed(ActionEvent e) {
320 new HideEvent().actionPerformed(null);
321 }
322 }
323
324 class FreezeMouseListener extends MouseAdapter {
325 @Override
326 public void mouseEntered(MouseEvent e) {
327 if (unfreezeDelayTimer.isRunning()) {
328 unfreezeDelayTimer.stop();
329 } else {
330 hideTimer.stop();
331 elapsedTime += System.currentTimeMillis() - displayTimeStart;
332 currentNotificationPanel.setNotificationBackground(PANEL_OPAQUE);
333 currentNotificationPanel.repaint();
334 }
335 }
336
337 @Override
338 public void mouseExited(MouseEvent e) {
339 unfreezeDelayTimer.restart();
340 }
341 }
342 }
343
344 /**
345 * A panel with rounded edges and line border.
346 */
347 public static class RoundedPanel extends JPanel {
348
349 RoundedPanel() {
350 super();
351 setOpaque(false);
352 }
353
354 @Override
355 protected void paintComponent(Graphics graphics) {
356 Graphics2D g = (Graphics2D) graphics;
357 g.setRenderingHint(
358 RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
359 g.setColor(getBackground());
360 float lineWidth = 1.4f;
361 Shape rect = new RoundRectangle2D.Double(
362 lineWidth/2d + getInsets().left,
363 lineWidth/2d + getInsets().top,
364 getWidth() - lineWidth/2d - getInsets().left - getInsets().right,
365 getHeight() - lineWidth/2d - getInsets().top - getInsets().bottom,
366 20, 20);
367
368 g.fill(rect);
369 g.setColor(getForeground());
370 g.setStroke(new BasicStroke(lineWidth));
371 g.draw(rect);
372 super.paintComponent(graphics);
373 }
374 }
375}
Note: See TracBrowser for help on using the repository browser.