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

Last change on this file since 11995 was 11396, checked in by Don-vip, 7 years ago

sonar - pmd:AccessorClassGeneration

  • Property svn:eol-style set to native
File size: 13.2 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.data.preferences.IntegerProperty;
40import org.openstreetmap.josm.gui.help.HelpBrowser;
41import org.openstreetmap.josm.gui.help.HelpUtil;
42import org.openstreetmap.josm.tools.ImageProvider;
43
44/**
45 * Manages {@link Notification}s, i.e. displays them on screen.
46 *
47 * Don't use this class directly, but use {@link Notification#show()}.
48 *
49 * If multiple messages are sent in a short period of time, they are put in
50 * a queue and displayed one after the other.
51 *
52 * The user can stop the timer (freeze the message) by moving the mouse cursor
53 * above the panel. As a visual cue, the background color changes from
54 * semi-transparent to opaque while the timer is frozen.
55 */
56class NotificationManager {
57
58 private final Timer hideTimer; // started when message is shown, responsible for hiding the message
59 private final Timer pauseTimer; // makes sure, there is a small pause between two consecutive messages
60 private final Timer unfreezeDelayTimer; // tiny delay before resuming the timer when mouse cursor 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 IntegerProperty pauseTime = new IntegerProperty("notification-default-pause-time-ms", 300); // 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 NotificationManager() {
78 queue = new LinkedList<>();
79 hideTimer = new Timer(Notification.TIME_DEFAULT, e -> this.stopHideTimer());
80 hideTimer.setRepeats(false);
81 pauseTimer = new Timer(pauseTime.get(), new PauseFinishedEvent());
82 pauseTimer.setRepeats(false);
83 unfreezeDelayTimer = new Timer(10, new UnfreezeEvent());
84 unfreezeDelayTimer.setRepeats(false);
85 }
86
87 /**
88 * Show the given notification
89 * @param note The note to show.
90 * @see Notification#show()
91 */
92 public void showNotification(Notification note) {
93 synchronized (queue) {
94 queue.add(note);
95 processQueue();
96 }
97 }
98
99 private void processQueue() {
100 if (running) return;
101
102 currentNotification = queue.poll();
103 if (currentNotification == null) return;
104
105 currentNotificationPanel = new NotificationPanel(currentNotification, new FreezeMouseListener(), e -> this.stopHideTimer());
106 currentNotificationPanel.validate();
107
108 int margin = 5;
109 JFrame parentWindow = (JFrame) Main.parent;
110 Dimension size = currentNotificationPanel.getPreferredSize();
111 if (parentWindow != null) {
112 int x;
113 int y;
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 }
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 void stopHideTimer() {
148 hideTimer.stop();
149 if (currentNotificationPanel != null) {
150 currentNotificationPanel.setVisible(false);
151 JFrame parent = (JFrame) Main.parent;
152 if (parent != null) {
153 parent.getLayeredPane().remove(currentNotificationPanel);
154 }
155 currentNotificationPanel = null;
156 }
157 pauseTimer.restart();
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 static class NotificationPanel extends JPanel {
184
185 static final class ShowNoteHelpAction extends AbstractAction {
186 private final Notification note;
187
188 ShowNoteHelpAction(Notification note) {
189 this.note = note;
190 }
191
192 @Override
193 public void actionPerformed(ActionEvent e) {
194 SwingUtilities.invokeLater(() -> HelpBrowser.setUrlForHelpTopic(note.getHelpTopic()));
195 }
196 }
197
198 private JPanel innerPanel;
199
200 NotificationPanel(Notification note, MouseListener freeze, ActionListener hideListener) {
201 setVisible(false);
202 build(note, freeze, hideListener);
203 }
204
205 public void setNotificationBackground(Color c) {
206 innerPanel.setBackground(c);
207 }
208
209 private void build(final Notification note, MouseListener freeze, ActionListener hideListener) {
210 JButton btnClose = new JButton();
211 btnClose.addActionListener(hideListener);
212 btnClose.setIcon(ImageProvider.get("misc", "grey_x"));
213 btnClose.setPreferredSize(new Dimension(50, 50));
214 btnClose.setMargin(new Insets(0, 0, 1, 1));
215 btnClose.setContentAreaFilled(false);
216 // put it in JToolBar to get a better appearance
217 JToolBar tbClose = new JToolBar();
218 tbClose.setFloatable(false);
219 tbClose.setBorderPainted(false);
220 tbClose.setOpaque(false);
221 tbClose.add(btnClose);
222
223 JToolBar tbHelp = null;
224 if (note.getHelpTopic() != null) {
225 JButton btnHelp = new JButton(tr("Help"));
226 btnHelp.setIcon(ImageProvider.get("help"));
227 btnHelp.setToolTipText(tr("Show help information"));
228 HelpUtil.setHelpContext(btnHelp, note.getHelpTopic());
229 btnHelp.addActionListener(new ShowNoteHelpAction(note));
230 btnHelp.setOpaque(false);
231 tbHelp = new JToolBar();
232 tbHelp.setFloatable(false);
233 tbHelp.setBorderPainted(false);
234 tbHelp.setOpaque(false);
235 tbHelp.add(btnHelp);
236 }
237
238 setOpaque(false);
239 innerPanel = new RoundedPanel();
240 innerPanel.setBackground(PANEL_SEMITRANSPARENT);
241 innerPanel.setForeground(Color.BLACK);
242
243 GroupLayout layout = new GroupLayout(innerPanel);
244 innerPanel.setLayout(layout);
245 layout.setAutoCreateGaps(true);
246 layout.setAutoCreateContainerGaps(true);
247
248 innerPanel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
249 add(innerPanel);
250
251 JLabel icon = null;
252 if (note.getIcon() != null) {
253 icon = new JLabel(note.getIcon());
254 }
255 Component content = note.getContent();
256 GroupLayout.SequentialGroup hgroup = layout.createSequentialGroup();
257 if (icon != null) {
258 hgroup.addComponent(icon);
259 }
260 if (tbHelp != null) {
261 hgroup.addGroup(layout.createParallelGroup(GroupLayout.Alignment.TRAILING)
262 .addComponent(content)
263 .addComponent(tbHelp)
264 );
265 } else {
266 hgroup.addComponent(content);
267 }
268 hgroup.addComponent(tbClose);
269 GroupLayout.ParallelGroup vgroup = layout.createParallelGroup();
270 if (icon != null) {
271 vgroup.addComponent(icon);
272 }
273 vgroup.addComponent(content);
274 vgroup.addComponent(tbClose);
275 layout.setHorizontalGroup(hgroup);
276
277 if (tbHelp != null) {
278 layout.setVerticalGroup(layout.createSequentialGroup()
279 .addGroup(vgroup)
280 .addComponent(tbHelp)
281 );
282 } else {
283 layout.setVerticalGroup(vgroup);
284 }
285
286 /*
287 * The timer stops when the mouse cursor is above the panel.
288 *
289 * This is not straightforward, because the JPanel will get a
290 * mouseExited event when the cursor moves on top of the JButton
291 * inside the panel.
292 *
293 * The current hacky solution is to register the freeze MouseListener
294 * not only to the panel, but to all the components inside the panel.
295 *
296 * Moving the mouse cursor from one component to the next would
297 * cause some flickering (timer is started and stopped for a fraction
298 * of a second, background color is switched twice), so there is
299 * a tiny delay before the timer really resumes.
300 */
301 addMouseListenerToAllChildComponents(this, freeze);
302 }
303
304 private static void addMouseListenerToAllChildComponents(Component comp, MouseListener listener) {
305 comp.addMouseListener(listener);
306 if (comp instanceof Container) {
307 for (Component c: ((Container) comp).getComponents()) {
308 addMouseListenerToAllChildComponents(c, listener);
309 }
310 }
311 }
312 }
313
314 class FreezeMouseListener extends MouseAdapter {
315 @Override
316 public void mouseEntered(MouseEvent e) {
317 if (unfreezeDelayTimer.isRunning()) {
318 unfreezeDelayTimer.stop();
319 } else {
320 hideTimer.stop();
321 elapsedTime += System.currentTimeMillis() - displayTimeStart;
322 currentNotificationPanel.setNotificationBackground(PANEL_OPAQUE);
323 currentNotificationPanel.repaint();
324 }
325 }
326
327 @Override
328 public void mouseExited(MouseEvent e) {
329 unfreezeDelayTimer.restart();
330 }
331 }
332
333 /**
334 * A panel with rounded edges and line border.
335 */
336 public static class RoundedPanel extends JPanel {
337
338 RoundedPanel() {
339 super();
340 setOpaque(false);
341 }
342
343 @Override
344 protected void paintComponent(Graphics graphics) {
345 Graphics2D g = (Graphics2D) graphics;
346 g.setRenderingHint(
347 RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
348 g.setColor(getBackground());
349 float lineWidth = 1.4f;
350 Shape rect = new RoundRectangle2D.Double(
351 lineWidth/2d + getInsets().left,
352 lineWidth/2d + getInsets().top,
353 getWidth() - lineWidth/2d - getInsets().left - getInsets().right,
354 getHeight() - lineWidth/2d - getInsets().top - getInsets().bottom,
355 20, 20);
356
357 g.fill(rect);
358 g.setColor(getForeground());
359 g.setStroke(new BasicStroke(lineWidth));
360 g.draw(rect);
361 super.paintComponent(graphics);
362 }
363 }
364
365 public static synchronized NotificationManager getInstance() {
366 if (instance == null) {
367 instance = new NotificationManager();
368 }
369 return instance;
370 }
371}
Note: See TracBrowser for help on using the repository browser.