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

Last change on this file since 6960 was 6960, checked in by Don-vip, 10 years ago

fix #8859, fix #9893 - Change of System of Measurement (SoM) in map status:

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