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 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}