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

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

see #15182 - deprecate Main.toolbar. Replacement: gui.MainApplication.getToolbar()

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