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 Timer hideTimer; // started when message is shown, responsible for hiding the message 058 private Timer pauseTimer; // makes sure, there is a small pause between two consecutive messages 059 private Timer unfreezeDelayTimer; // tiny delay before resuming the timer when mouse cursor 060 // is moved off the panel 061 private boolean running; 062 063 private Notification currentNotification; 064 private NotificationPanel currentNotificationPanel; 065 private final Queue<Notification> queue; 066 067 private static int pauseTime = Main.pref.getInteger("notification-default-pause-time-ms", 300); // milliseconds 068 static int defaultNotificationTime = Main.pref.getInteger("notification-default-time-ms", 5000); // milliseconds 069 070 private long displayTimeStart; 071 private long elapsedTime; 072 073 private static NotificationManager INSTANCE; 074 075 private static final Color PANEL_SEMITRANSPARENT = new Color(224, 236, 249, 230); 076 private static final Color PANEL_OPAQUE = new Color(224, 236, 249); 077 078 public static synchronized NotificationManager getInstance() { 079 if (INSTANCE == null) { 080 INSTANCE = new NotificationManager(); 081 } 082 return INSTANCE; 083 } 084 085 NotificationManager() { 086 queue = new LinkedList<>(); 087 hideTimer = new Timer(defaultNotificationTime, new HideEvent()); 088 hideTimer.setRepeats(false); 089 pauseTimer = new Timer(pauseTime, new PauseFinishedEvent()); 090 pauseTimer.setRepeats(false); 091 unfreezeDelayTimer = new Timer(10, new UnfreezeEvent()); 092 unfreezeDelayTimer.setRepeats(false); 093 } 094 095 public void showNotification(Notification note) { 096 synchronized (queue) { 097 queue.add(note); 098 processQueue(); 099 } 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 if (currentNotificationPanel != null) { 153 currentNotificationPanel.setVisible(false); 154 ((JFrame) Main.parent).getLayeredPane().remove(currentNotificationPanel); 155 currentNotificationPanel = null; 156 } 157 pauseTimer.restart(); 158 } 159 } 160 161 private class PauseFinishedEvent implements ActionListener { 162 163 @Override 164 public void actionPerformed(ActionEvent e) { 165 synchronized (queue) { 166 running = false; 167 processQueue(); 168 } 169 } 170 } 171 172 private class UnfreezeEvent implements ActionListener { 173 174 @Override 175 public void actionPerformed(ActionEvent e) { 176 if (currentNotificationPanel != null) { 177 currentNotificationPanel.setNotificationBackground(PANEL_SEMITRANSPARENT); 178 currentNotificationPanel.repaint(); 179 } 180 startHideTimer(); 181 } 182 } 183 184 private class NotificationPanel extends JPanel { 185 186 private JPanel innerPanel; 187 188 NotificationPanel(Notification note) { 189 setVisible(false); 190 build(note); 191 } 192 193 public void setNotificationBackground(Color c) { 194 innerPanel.setBackground(c); 195 } 196 197 private void build(final Notification note) { 198 JButton btnClose = new JButton(new HideAction()); 199 btnClose.setPreferredSize(new Dimension(50, 50)); 200 btnClose.setMargin(new Insets(0, 0, 1, 1)); 201 btnClose.setContentAreaFilled(false); 202 // put it in JToolBar to get a better appearance 203 JToolBar tbClose = new JToolBar(); 204 tbClose.setFloatable(false); 205 tbClose.setBorderPainted(false); 206 tbClose.setOpaque(false); 207 tbClose.add(btnClose); 208 209 JToolBar tbHelp = null; 210 if (note.getHelpTopic() != null) { 211 JButton btnHelp = new JButton(tr("Help")); 212 btnHelp.setIcon(ImageProvider.get("help")); 213 btnHelp.setToolTipText(tr("Show help information")); 214 HelpUtil.setHelpContext(btnHelp, note.getHelpTopic()); 215 btnHelp.addActionListener(new AbstractAction() { 216 @Override 217 public void actionPerformed(ActionEvent e) { 218 SwingUtilities.invokeLater(new Runnable() { 219 @Override 220 public void run() { 221 HelpBrowser.setUrlForHelpTopic(note.getHelpTopic()); 222 } 223 }); 224 } 225 }); 226 btnHelp.setOpaque(false); 227 tbHelp = new JToolBar(); 228 tbHelp.setFloatable(false); 229 tbHelp.setBorderPainted(false); 230 tbHelp.setOpaque(false); 231 tbHelp.add(btnHelp); 232 } 233 234 setOpaque(false); 235 innerPanel = new RoundedPanel(); 236 innerPanel.setBackground(PANEL_SEMITRANSPARENT); 237 innerPanel.setForeground(Color.BLACK); 238 239 GroupLayout layout = new GroupLayout(innerPanel); 240 innerPanel.setLayout(layout); 241 layout.setAutoCreateGaps(true); 242 layout.setAutoCreateContainerGaps(true); 243 244 innerPanel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); 245 add(innerPanel); 246 247 JLabel icon = null; 248 if (note.getIcon() != null) { 249 icon = new JLabel(note.getIcon()); 250 } 251 Component content = note.getContent(); 252 GroupLayout.SequentialGroup hgroup = layout.createSequentialGroup(); 253 if (icon != null) { 254 hgroup.addComponent(icon); 255 } 256 if (tbHelp != null) { 257 hgroup.addGroup(layout.createParallelGroup(GroupLayout.Alignment.TRAILING) 258 .addComponent(content) 259 .addComponent(tbHelp) 260 ); 261 } else { 262 hgroup.addComponent(content); 263 } 264 hgroup.addComponent(tbClose); 265 GroupLayout.ParallelGroup vgroup = layout.createParallelGroup(); 266 if (icon != null) { 267 vgroup.addComponent(icon); 268 } 269 vgroup.addComponent(content); 270 vgroup.addComponent(tbClose); 271 layout.setHorizontalGroup(hgroup); 272 273 if (tbHelp != null) { 274 layout.setVerticalGroup(layout.createSequentialGroup() 275 .addGroup(vgroup) 276 .addComponent(tbHelp) 277 ); 278 } else { 279 layout.setVerticalGroup(vgroup); 280 } 281 282 /* 283 * The timer stops when the mouse cursor is above the panel. 284 * 285 * This is not straightforward, because the JPanel will get a 286 * mouseExited event when the cursor moves on top of the JButton 287 * inside the panel. 288 * 289 * The current hacky solution is to register the freeze MouseListener 290 * not only to the panel, but to all the components inside the panel. 291 * 292 * Moving the mouse cursor from one component to the next would 293 * cause some flickering (timer is started and stopped for a fraction 294 * of a second, background color is switched twice), so there is 295 * a tiny delay before the timer really resumes. 296 */ 297 MouseListener freeze = new FreezeMouseListener(); 298 addMouseListenerToAllChildComponents(this, freeze); 299 } 300 301 private void addMouseListenerToAllChildComponents(Component comp, MouseListener listener) { 302 comp.addMouseListener(listener); 303 if (comp instanceof Container) { 304 for (Component c: ((Container) comp).getComponents()) { 305 addMouseListenerToAllChildComponents(c, listener); 306 } 307 } 308 } 309 310 class HideAction extends AbstractAction { 311 312 HideAction() { 313 putValue(SMALL_ICON, ImageProvider.get("misc", "grey_x")); 314 } 315 316 @Override 317 public void actionPerformed(ActionEvent e) { 318 new HideEvent().actionPerformed(null); 319 } 320 } 321 322 class FreezeMouseListener extends MouseAdapter { 323 @Override 324 public void mouseEntered(MouseEvent e) { 325 if (unfreezeDelayTimer.isRunning()) { 326 unfreezeDelayTimer.stop(); 327 } else { 328 hideTimer.stop(); 329 elapsedTime += System.currentTimeMillis() - displayTimeStart; 330 currentNotificationPanel.setNotificationBackground(PANEL_OPAQUE); 331 currentNotificationPanel.repaint(); 332 } 333 } 334 335 @Override 336 public void mouseExited(MouseEvent e) { 337 unfreezeDelayTimer.restart(); 338 } 339 } 340 } 341 342 /** 343 * A panel with rounded edges and line border. 344 */ 345 public static class RoundedPanel extends JPanel { 346 347 RoundedPanel() { 348 super(); 349 setOpaque(false); 350 } 351 352 @Override 353 protected void paintComponent(Graphics graphics) { 354 Graphics2D g = (Graphics2D) graphics; 355 g.setRenderingHint( 356 RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); 357 g.setColor(getBackground()); 358 float lineWidth = 1.4f; 359 Shape rect = new RoundRectangle2D.Double( 360 lineWidth/2d + getInsets().left, 361 lineWidth/2d + getInsets().top, 362 getWidth() - lineWidth/2d - getInsets().left - getInsets().right, 363 getHeight() - lineWidth/2d - getInsets().top - getInsets().bottom, 364 20, 20); 365 366 g.fill(rect); 367 g.setColor(getForeground()); 368 g.setStroke(new BasicStroke(lineWidth)); 369 g.draw(rect); 370 super.paintComponent(graphics); 371 } 372 } 373}