| 1 | // License: GPL. See LICENSE file for details. |
|---|
| 2 | package org.openstreetmap.josm.gui; |
|---|
| 3 | |
|---|
| 4 | import static org.openstreetmap.josm.gui.help.HelpUtil.ht; |
|---|
| 5 | import static org.openstreetmap.josm.tools.I18n.tr; |
|---|
| 6 | |
|---|
| 7 | import java.awt.AWTEvent; |
|---|
| 8 | import java.awt.Color; |
|---|
| 9 | import java.awt.Component; |
|---|
| 10 | import java.awt.Cursor; |
|---|
| 11 | import java.awt.Dimension; |
|---|
| 12 | import java.awt.EventQueue; |
|---|
| 13 | import java.awt.Font; |
|---|
| 14 | import java.awt.GridBagLayout; |
|---|
| 15 | import java.awt.Point; |
|---|
| 16 | import java.awt.SystemColor; |
|---|
| 17 | import java.awt.Toolkit; |
|---|
| 18 | import java.awt.event.AWTEventListener; |
|---|
| 19 | import java.awt.event.InputEvent; |
|---|
| 20 | import java.awt.event.KeyAdapter; |
|---|
| 21 | import java.awt.event.KeyEvent; |
|---|
| 22 | import java.awt.event.MouseAdapter; |
|---|
| 23 | import java.awt.event.MouseEvent; |
|---|
| 24 | import java.awt.event.MouseListener; |
|---|
| 25 | import java.awt.event.MouseMotionListener; |
|---|
| 26 | import java.util.ArrayList; |
|---|
| 27 | import java.util.Collection; |
|---|
| 28 | import java.util.ConcurrentModificationException; |
|---|
| 29 | import java.util.List; |
|---|
| 30 | |
|---|
| 31 | import javax.swing.BorderFactory; |
|---|
| 32 | import javax.swing.JLabel; |
|---|
| 33 | import javax.swing.JPanel; |
|---|
| 34 | import javax.swing.JProgressBar; |
|---|
| 35 | import javax.swing.JScrollPane; |
|---|
| 36 | import javax.swing.JTextField; |
|---|
| 37 | import javax.swing.Popup; |
|---|
| 38 | import javax.swing.PopupFactory; |
|---|
| 39 | import javax.swing.UIManager; |
|---|
| 40 | |
|---|
| 41 | import org.openstreetmap.josm.Main; |
|---|
| 42 | import org.openstreetmap.josm.data.coor.CoordinateFormat; |
|---|
| 43 | import org.openstreetmap.josm.data.coor.LatLon; |
|---|
| 44 | import org.openstreetmap.josm.data.osm.DataSet; |
|---|
| 45 | import org.openstreetmap.josm.data.osm.OsmPrimitive; |
|---|
| 46 | import org.openstreetmap.josm.gui.help.Helpful; |
|---|
| 47 | import org.openstreetmap.josm.gui.progress.PleaseWaitProgressMonitor; |
|---|
| 48 | import org.openstreetmap.josm.gui.progress.PleaseWaitProgressMonitor.ProgressMonitorDialog; |
|---|
| 49 | import org.openstreetmap.josm.tools.GBC; |
|---|
| 50 | import org.openstreetmap.josm.tools.ImageProvider; |
|---|
| 51 | |
|---|
| 52 | /** |
|---|
| 53 | * A component that manages some status information display about the map. |
|---|
| 54 | * It keeps a status line below the map up to date and displays some tooltip |
|---|
| 55 | * information if the user hold the mouse long enough at some point. |
|---|
| 56 | * |
|---|
| 57 | * All this is done in background to not disturb other processes. |
|---|
| 58 | * |
|---|
| 59 | * The background thread does not alter any data of the map (read only thread). |
|---|
| 60 | * Also it is rather fail safe. In case of some error in the data, it just does |
|---|
| 61 | * nothing instead of whining and complaining. |
|---|
| 62 | * |
|---|
| 63 | * @author imi |
|---|
| 64 | */ |
|---|
| 65 | public class MapStatus extends JPanel implements Helpful { |
|---|
| 66 | |
|---|
| 67 | /** |
|---|
| 68 | * The MapView this status belongs to. |
|---|
| 69 | */ |
|---|
| 70 | final MapView mv; |
|---|
| 71 | final Collector collector; |
|---|
| 72 | |
|---|
| 73 | /** |
|---|
| 74 | * A small user interface component that consists of an image label and |
|---|
| 75 | * a fixed text content to the right of the image. |
|---|
| 76 | */ |
|---|
| 77 | static class ImageLabel extends JPanel { |
|---|
| 78 | static Color backColor = Color.decode("#b8cfe5"); |
|---|
| 79 | static Color backColorActive = Color.decode("#aaff5e"); |
|---|
| 80 | |
|---|
| 81 | private JLabel tf; |
|---|
| 82 | private int chars; |
|---|
| 83 | public ImageLabel(String img, String tooltip, int chars) { |
|---|
| 84 | super(); |
|---|
| 85 | setLayout(new GridBagLayout()); |
|---|
| 86 | setBackground(backColor); |
|---|
| 87 | add(new JLabel(ImageProvider.get("statusline/"+img+".png")), GBC.std().anchor(GBC.WEST).insets(0,1,1,0)); |
|---|
| 88 | add(tf = new JLabel(), GBC.std().fill(GBC.BOTH).anchor(GBC.WEST).insets(2,1,1,0)); |
|---|
| 89 | setToolTipText(tooltip); |
|---|
| 90 | this.chars = chars; |
|---|
| 91 | } |
|---|
| 92 | public void setText(String t) { |
|---|
| 93 | tf.setText(t); |
|---|
| 94 | } |
|---|
| 95 | @Override public Dimension getPreferredSize() { |
|---|
| 96 | return new Dimension(25 + chars*tf.getFontMetrics(tf.getFont()).charWidth('0'), super.getPreferredSize().height); |
|---|
| 97 | } |
|---|
| 98 | @Override public Dimension getMinimumSize() { |
|---|
| 99 | return new Dimension(25 + chars*tf.getFontMetrics(tf.getFont()).charWidth('0'), super.getMinimumSize().height); |
|---|
| 100 | } |
|---|
| 101 | } |
|---|
| 102 | |
|---|
| 103 | public class BackgroundProgressMonitor implements ProgressMonitorDialog { |
|---|
| 104 | |
|---|
| 105 | private String title; |
|---|
| 106 | private String customText; |
|---|
| 107 | |
|---|
| 108 | private void updateText() { |
|---|
| 109 | if (customText != null && !customText.isEmpty()) { |
|---|
| 110 | progressBar.setToolTipText(tr("{0} ({1})", title, customText)); |
|---|
| 111 | } else { |
|---|
| 112 | progressBar.setToolTipText(title); |
|---|
| 113 | } |
|---|
| 114 | } |
|---|
| 115 | |
|---|
| 116 | public void setVisible(boolean visible) { |
|---|
| 117 | progressBar.setVisible(visible); |
|---|
| 118 | } |
|---|
| 119 | |
|---|
| 120 | public void updateProgress(int progress) { |
|---|
| 121 | progressBar.setValue(progress); |
|---|
| 122 | MapStatus.this.doLayout(); |
|---|
| 123 | } |
|---|
| 124 | |
|---|
| 125 | public void setCustomText(String text) { |
|---|
| 126 | this.customText = text; |
|---|
| 127 | updateText(); |
|---|
| 128 | } |
|---|
| 129 | |
|---|
| 130 | public void setCurrentAction(String text) { |
|---|
| 131 | this.title = text; |
|---|
| 132 | updateText(); |
|---|
| 133 | } |
|---|
| 134 | |
|---|
| 135 | public void setIndeterminate(boolean newValue) { |
|---|
| 136 | UIManager.put("ProgressBar.cycleTime", UIManager.getInt("ProgressBar.repaintInterval") * 100); |
|---|
| 137 | progressBar.setIndeterminate(newValue); |
|---|
| 138 | } |
|---|
| 139 | |
|---|
| 140 | @Override |
|---|
| 141 | public void appendLogMessage(String message) { |
|---|
| 142 | if (message != null && !message.isEmpty()) { |
|---|
| 143 | System.out.println("appendLogMessage not implemented for background tasks. Message was: " + message); |
|---|
| 144 | } |
|---|
| 145 | } |
|---|
| 146 | |
|---|
| 147 | } |
|---|
| 148 | |
|---|
| 149 | final ImageLabel lonText = new ImageLabel("lon", tr("The geographic longitude at the mouse pointer."), 11); |
|---|
| 150 | final ImageLabel nameText = new ImageLabel("name", tr("The name of the object at the mouse pointer."), 20); |
|---|
| 151 | final JTextField helpText = new JTextField(); |
|---|
| 152 | final ImageLabel latText = new ImageLabel("lat", tr("The geographic latitude at the mouse pointer."), 11); |
|---|
| 153 | final ImageLabel angleText = new ImageLabel("angle", tr("The angle between the previous and the current way segment."), 6); |
|---|
| 154 | final ImageLabel headingText = new ImageLabel("heading", tr("The (compass) heading of the line segment being drawn."), 6); |
|---|
| 155 | final ImageLabel distText = new ImageLabel("dist", tr("The length of the new way segment being drawn."), 10); |
|---|
| 156 | final JProgressBar progressBar = new JProgressBar(); |
|---|
| 157 | public final BackgroundProgressMonitor progressMonitor = new BackgroundProgressMonitor(); |
|---|
| 158 | |
|---|
| 159 | /** |
|---|
| 160 | * This is the thread that runs in the background and collects the information displayed. |
|---|
| 161 | * It gets destroyed by MapFrame.java/destroy() when the MapFrame itself is destroyed. |
|---|
| 162 | */ |
|---|
| 163 | public Thread thread; |
|---|
| 164 | |
|---|
| 165 | private final List<StatusTextHistory> statusText = new ArrayList<StatusTextHistory>(); |
|---|
| 166 | |
|---|
| 167 | private static class StatusTextHistory { |
|---|
| 168 | final Object id; |
|---|
| 169 | final String text; |
|---|
| 170 | |
|---|
| 171 | public StatusTextHistory(Object id, String text) { |
|---|
| 172 | this.id = id; |
|---|
| 173 | this.text = text; |
|---|
| 174 | } |
|---|
| 175 | |
|---|
| 176 | @Override |
|---|
| 177 | public boolean equals(Object obj) { |
|---|
| 178 | return obj instanceof StatusTextHistory && ((StatusTextHistory)obj).id == id; |
|---|
| 179 | } |
|---|
| 180 | |
|---|
| 181 | @Override |
|---|
| 182 | public int hashCode() { |
|---|
| 183 | return System.identityHashCode(id); |
|---|
| 184 | } |
|---|
| 185 | } |
|---|
| 186 | |
|---|
| 187 | /** |
|---|
| 188 | * The collector class that waits for notification and then update |
|---|
| 189 | * the display objects. |
|---|
| 190 | * |
|---|
| 191 | * @author imi |
|---|
| 192 | */ |
|---|
| 193 | private final class Collector implements Runnable { |
|---|
| 194 | /** |
|---|
| 195 | * the mouse position of the previous iteration. This is used to show |
|---|
| 196 | * the popup until the cursor is moved. |
|---|
| 197 | */ |
|---|
| 198 | private Point oldMousePos; |
|---|
| 199 | /** |
|---|
| 200 | * Contains the labels that are currently shown in the information |
|---|
| 201 | * popup |
|---|
| 202 | */ |
|---|
| 203 | private List<JLabel> popupLabels = null; |
|---|
| 204 | /** |
|---|
| 205 | * The popup displayed to show additional information |
|---|
| 206 | */ |
|---|
| 207 | private Popup popup; |
|---|
| 208 | |
|---|
| 209 | private MapFrame parent; |
|---|
| 210 | |
|---|
| 211 | public Collector(MapFrame parent) { |
|---|
| 212 | this.parent = parent; |
|---|
| 213 | } |
|---|
| 214 | |
|---|
| 215 | /** |
|---|
| 216 | * Execution function for the Collector. |
|---|
| 217 | */ |
|---|
| 218 | public void run() { |
|---|
| 219 | registerListeners(); |
|---|
| 220 | try { |
|---|
| 221 | for (;;) { |
|---|
| 222 | |
|---|
| 223 | final MouseState ms = new MouseState(); |
|---|
| 224 | synchronized (this) { |
|---|
| 225 | // TODO Would be better if the timeout wasn't necessary |
|---|
| 226 | try {wait(1000);} catch (InterruptedException e) {} |
|---|
| 227 | ms.modifiers = mouseState.modifiers; |
|---|
| 228 | ms.mousePos = mouseState.mousePos; |
|---|
| 229 | } |
|---|
| 230 | if (parent != Main.map) |
|---|
| 231 | return; // exit, if new parent. |
|---|
| 232 | |
|---|
| 233 | // Do nothing, if required data is missing |
|---|
| 234 | if(ms.mousePos == null || mv.center == null) { |
|---|
| 235 | continue; |
|---|
| 236 | } |
|---|
| 237 | |
|---|
| 238 | try { |
|---|
| 239 | EventQueue.invokeAndWait(new Runnable() { |
|---|
| 240 | |
|---|
| 241 | @Override |
|---|
| 242 | public void run() { |
|---|
| 243 | // Freeze display when holding down CTRL |
|---|
| 244 | if ((ms.modifiers & MouseEvent.CTRL_DOWN_MASK) != 0) { |
|---|
| 245 | // update the information popup's labels though, because |
|---|
| 246 | // the selection might have changed from the outside |
|---|
| 247 | popupUpdateLabels(); |
|---|
| 248 | return; |
|---|
| 249 | } |
|---|
| 250 | |
|---|
| 251 | // This try/catch is a hack to stop the flooding bug reports about this. |
|---|
| 252 | // The exception needed to handle with in the first place, means that this |
|---|
| 253 | // access to the data need to be restarted, if the main thread modifies |
|---|
| 254 | // the data. |
|---|
| 255 | DataSet ds = null; |
|---|
| 256 | // The popup != null check is required because a left-click |
|---|
| 257 | // produces several events as well, which would make this |
|---|
| 258 | // variable true. Of course we only want the popup to show |
|---|
| 259 | // if the middle mouse button has been pressed in the first |
|---|
| 260 | // place |
|---|
| 261 | boolean mouseNotMoved = oldMousePos != null |
|---|
| 262 | && oldMousePos.equals(ms.mousePos); |
|---|
| 263 | boolean isAtOldPosition = mouseNotMoved && popup != null; |
|---|
| 264 | boolean middleMouseDown = (ms.modifiers & MouseEvent.BUTTON2_DOWN_MASK) != 0; |
|---|
| 265 | try { |
|---|
| 266 | ds = mv.getCurrentDataSet(); |
|---|
| 267 | if (ds != null) { |
|---|
| 268 | // This is not perfect, if current dataset was changed during execution, the lock would be useless |
|---|
| 269 | if(isAtOldPosition && middleMouseDown) { |
|---|
| 270 | // Write lock is necessary when selecting in popupCycleSelection |
|---|
| 271 | // locks can not be upgraded -> if do read lock here and write lock later (in OsmPrimitive.updateFlags) |
|---|
| 272 | // then always occurs deadlock (#5814) |
|---|
| 273 | ds.beginUpdate(); |
|---|
| 274 | } else { |
|---|
| 275 | ds.getReadLock().lock(); |
|---|
| 276 | } |
|---|
| 277 | } |
|---|
| 278 | |
|---|
| 279 | // Set the text label in the bottom status bar |
|---|
| 280 | // "if mouse moved only" was added to stop heap growing |
|---|
| 281 | if (!mouseNotMoved) statusBarElementUpdate(ms); |
|---|
| 282 | |
|---|
| 283 | |
|---|
| 284 | // Popup Information |
|---|
| 285 | // display them if the middle mouse button is pressed and |
|---|
| 286 | // keep them until the mouse is moved |
|---|
| 287 | if (middleMouseDown || isAtOldPosition) |
|---|
| 288 | { |
|---|
| 289 | Collection<OsmPrimitive> osms = mv.getAllNearest(ms.mousePos, OsmPrimitive.isUsablePredicate); |
|---|
| 290 | |
|---|
| 291 | if (osms == null) |
|---|
| 292 | return; |
|---|
| 293 | |
|---|
| 294 | final JPanel c = new JPanel(new GridBagLayout()); |
|---|
| 295 | final JLabel lbl = new JLabel( |
|---|
| 296 | "<html>"+tr("Middle click again to cycle through.<br>"+ |
|---|
| 297 | "Hold CTRL to select directly from this list with the mouse.<hr>")+"</html>", |
|---|
| 298 | null, |
|---|
| 299 | JLabel.HORIZONTAL |
|---|
| 300 | ); |
|---|
| 301 | lbl.setHorizontalAlignment(JLabel.LEFT); |
|---|
| 302 | c.add(lbl, GBC.eol().insets(2, 0, 2, 0)); |
|---|
| 303 | |
|---|
| 304 | // Only cycle if the mouse has not been moved and the |
|---|
| 305 | // middle mouse button has been pressed at least twice |
|---|
| 306 | // (the reason for this is the popup != null check for |
|---|
| 307 | // isAtOldPosition, see above. This is a nice side |
|---|
| 308 | // effect though, because it does not change selection |
|---|
| 309 | // of the first middle click) |
|---|
| 310 | if(isAtOldPosition && middleMouseDown) { |
|---|
| 311 | // Hand down mouse modifiers so the SHIFT mod can be |
|---|
| 312 | // handled correctly (see funcion) |
|---|
| 313 | popupCycleSelection(osms, ms.modifiers); |
|---|
| 314 | } |
|---|
| 315 | |
|---|
| 316 | // These labels may need to be updated from the outside |
|---|
| 317 | // so collect them |
|---|
| 318 | List<JLabel> lbls = new ArrayList<JLabel>(); |
|---|
| 319 | for (final OsmPrimitive osm : osms) { |
|---|
| 320 | JLabel l = popupBuildPrimitiveLabels(osm); |
|---|
| 321 | lbls.add(l); |
|---|
| 322 | c.add(l, GBC.eol().fill(GBC.HORIZONTAL).insets(2, 0, 2, 2)); |
|---|
| 323 | } |
|---|
| 324 | |
|---|
| 325 | popupShowPopup(popupCreatePopup(c, ms), lbls); |
|---|
| 326 | } else { |
|---|
| 327 | popupHidePopup(); |
|---|
| 328 | } |
|---|
| 329 | |
|---|
| 330 | oldMousePos = ms.mousePos; |
|---|
| 331 | } catch (ConcurrentModificationException x) { |
|---|
| 332 | //x.printStackTrace(); |
|---|
| 333 | } catch (NullPointerException x) { |
|---|
| 334 | //x.printStackTrace(); |
|---|
| 335 | } finally { |
|---|
| 336 | if (ds != null) { |
|---|
| 337 | if(isAtOldPosition && middleMouseDown) { |
|---|
| 338 | ds.endUpdate(); |
|---|
| 339 | } else { |
|---|
| 340 | ds.getReadLock().unlock(); |
|---|
| 341 | } |
|---|
| 342 | } |
|---|
| 343 | } |
|---|
| 344 | } |
|---|
| 345 | }); |
|---|
| 346 | } catch (Exception e) { |
|---|
| 347 | |
|---|
| 348 | } |
|---|
| 349 | } |
|---|
| 350 | } finally { |
|---|
| 351 | unregisterListeners(); |
|---|
| 352 | } |
|---|
| 353 | } |
|---|
| 354 | |
|---|
| 355 | /** |
|---|
| 356 | * Creates a popup for the given content next to the cursor. Tries to |
|---|
| 357 | * keep the popup on screen and shows a vertical scrollbar, if the |
|---|
| 358 | * screen is too small. |
|---|
| 359 | * @param content |
|---|
| 360 | * @param ms |
|---|
| 361 | * @return popup |
|---|
| 362 | */ |
|---|
| 363 | private final Popup popupCreatePopup(Component content, MouseState ms) { |
|---|
| 364 | Point p = mv.getLocationOnScreen(); |
|---|
| 365 | Dimension scrn = Toolkit.getDefaultToolkit().getScreenSize(); |
|---|
| 366 | |
|---|
| 367 | // Create a JScrollPane around the content, in case there's not |
|---|
| 368 | // enough space |
|---|
| 369 | JScrollPane sp = new JScrollPane(content); |
|---|
| 370 | sp.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); |
|---|
| 371 | sp.setBorder(BorderFactory.createRaisedBevelBorder()); |
|---|
| 372 | // Implement max-size content-independent |
|---|
| 373 | Dimension prefsize = sp.getPreferredSize(); |
|---|
| 374 | int w = Math.min(prefsize.width, Math.min(800, (scrn.width/2) - 16)); |
|---|
| 375 | int h = Math.min(prefsize.height, scrn.height - 10); |
|---|
| 376 | sp.setPreferredSize(new Dimension(w, h)); |
|---|
| 377 | |
|---|
| 378 | int xPos = p.x + ms.mousePos.x + 16; |
|---|
| 379 | // Display the popup to the left of the cursor if it would be cut |
|---|
| 380 | // off on its right, but only if more space is available |
|---|
| 381 | if(xPos + w > scrn.width && xPos > scrn.width/2) { |
|---|
| 382 | xPos = p.x + ms.mousePos.x - 4 - w; |
|---|
| 383 | } |
|---|
| 384 | int yPos = p.y + ms.mousePos.y + 16; |
|---|
| 385 | // Move the popup up if it would be cut off at its bottom but do not |
|---|
| 386 | // move it off screen on the top |
|---|
| 387 | if(yPos + h > scrn.height - 5) { |
|---|
| 388 | yPos = Math.max(5, scrn.height - h - 5); |
|---|
| 389 | } |
|---|
| 390 | |
|---|
| 391 | PopupFactory pf = PopupFactory.getSharedInstance(); |
|---|
| 392 | return pf.getPopup(mv, sp, xPos, yPos); |
|---|
| 393 | } |
|---|
| 394 | |
|---|
| 395 | /** |
|---|
| 396 | * Calls this to update the element that is shown in the statusbar |
|---|
| 397 | * @param ms |
|---|
| 398 | */ |
|---|
| 399 | private final void statusBarElementUpdate(MouseState ms) { |
|---|
| 400 | final OsmPrimitive osmNearest = mv.getNearestNodeOrWay(ms.mousePos, OsmPrimitive.isUsablePredicate, false); |
|---|
| 401 | if (osmNearest != null) { |
|---|
| 402 | nameText.setText(osmNearest.getDisplayName(DefaultNameFormatter.getInstance())); |
|---|
| 403 | } else { |
|---|
| 404 | nameText.setText(tr("(no object)")); |
|---|
| 405 | } |
|---|
| 406 | } |
|---|
| 407 | |
|---|
| 408 | /** |
|---|
| 409 | * Call this with a set of primitives to cycle through them. Method |
|---|
| 410 | * will automatically select the next item and update the map |
|---|
| 411 | * @param osms |
|---|
| 412 | * @param mouse modifiers |
|---|
| 413 | */ |
|---|
| 414 | private final void popupCycleSelection(Collection<OsmPrimitive> osms, int mods) { |
|---|
| 415 | DataSet ds = Main.main.getCurrentDataSet(); |
|---|
| 416 | // Find some items that are required for cycling through |
|---|
| 417 | OsmPrimitive firstItem = null; |
|---|
| 418 | OsmPrimitive firstSelected = null; |
|---|
| 419 | OsmPrimitive nextSelected = null; |
|---|
| 420 | for (final OsmPrimitive osm : osms) { |
|---|
| 421 | if(firstItem == null) { |
|---|
| 422 | firstItem = osm; |
|---|
| 423 | } |
|---|
| 424 | if(firstSelected != null && nextSelected == null) { |
|---|
| 425 | nextSelected = osm; |
|---|
| 426 | } |
|---|
| 427 | if(firstSelected == null && ds.isSelected(osm)) { |
|---|
| 428 | firstSelected = osm; |
|---|
| 429 | } |
|---|
| 430 | } |
|---|
| 431 | |
|---|
| 432 | // Clear previous selection if SHIFT (add to selection) is not |
|---|
| 433 | // pressed. Cannot use "setSelected()" because it will cause a |
|---|
| 434 | // fireSelectionChanged event which is unnecessary at this point. |
|---|
| 435 | if((mods & MouseEvent.SHIFT_DOWN_MASK) == 0) { |
|---|
| 436 | ds.clearSelection(); |
|---|
| 437 | } |
|---|
| 438 | |
|---|
| 439 | // This will cycle through the available items. |
|---|
| 440 | if(firstSelected == null) { |
|---|
| 441 | ds.addSelected(firstItem); |
|---|
| 442 | } else { |
|---|
| 443 | ds.clearSelection(firstSelected); |
|---|
| 444 | if(nextSelected != null) { |
|---|
| 445 | ds.addSelected(nextSelected); |
|---|
| 446 | } |
|---|
| 447 | } |
|---|
| 448 | } |
|---|
| 449 | |
|---|
| 450 | /** |
|---|
| 451 | * Tries to hide the given popup |
|---|
| 452 | * @param popup |
|---|
| 453 | */ |
|---|
| 454 | private final void popupHidePopup() { |
|---|
| 455 | popupLabels = null; |
|---|
| 456 | if(popup == null) |
|---|
| 457 | return; |
|---|
| 458 | final Popup staticPopup = popup; |
|---|
| 459 | popup = null; |
|---|
| 460 | EventQueue.invokeLater(new Runnable(){ |
|---|
| 461 | public void run() { staticPopup.hide(); }}); |
|---|
| 462 | } |
|---|
| 463 | |
|---|
| 464 | /** |
|---|
| 465 | * Tries to show the given popup, can be hidden using popupHideOldPopup |
|---|
| 466 | * If an old popup exists, it will be automatically hidden |
|---|
| 467 | * @param popup |
|---|
| 468 | */ |
|---|
| 469 | private final void popupShowPopup(Popup newPopup, List<JLabel> lbls) { |
|---|
| 470 | final Popup staticPopup = newPopup; |
|---|
| 471 | if(this.popup != null) { |
|---|
| 472 | // If an old popup exists, remove it when the new popup has been |
|---|
| 473 | // drawn to keep flickering to a minimum |
|---|
| 474 | final Popup staticOldPopup = this.popup; |
|---|
| 475 | EventQueue.invokeLater(new Runnable(){ |
|---|
| 476 | public void run() { |
|---|
| 477 | staticPopup.show(); |
|---|
| 478 | staticOldPopup.hide(); |
|---|
| 479 | } |
|---|
| 480 | }); |
|---|
| 481 | } else { |
|---|
| 482 | // There is no old popup |
|---|
| 483 | EventQueue.invokeLater(new Runnable(){ |
|---|
| 484 | public void run() { staticPopup.show(); }}); |
|---|
| 485 | } |
|---|
| 486 | this.popupLabels = lbls; |
|---|
| 487 | this.popup = newPopup; |
|---|
| 488 | } |
|---|
| 489 | |
|---|
| 490 | /** |
|---|
| 491 | * This method should be called if the selection may have changed from |
|---|
| 492 | * outside of this class. This is the case when CTRL is pressed and the |
|---|
| 493 | * user clicks on the map instead of the popup. |
|---|
| 494 | */ |
|---|
| 495 | private final void popupUpdateLabels() { |
|---|
| 496 | if(this.popup == null || this.popupLabels == null) |
|---|
| 497 | return; |
|---|
| 498 | for(JLabel l : this.popupLabels) { |
|---|
| 499 | l.validate(); |
|---|
| 500 | } |
|---|
| 501 | } |
|---|
| 502 | |
|---|
| 503 | /** |
|---|
| 504 | * Sets the colors for the given label depending on the selected status of |
|---|
| 505 | * the given OsmPrimitive |
|---|
| 506 | * |
|---|
| 507 | * @param lbl The label to color |
|---|
| 508 | * @param osm The primitive to derive the colors from |
|---|
| 509 | */ |
|---|
| 510 | private final void popupSetLabelColors(JLabel lbl, OsmPrimitive osm) { |
|---|
| 511 | DataSet ds = Main.main.getCurrentDataSet(); |
|---|
| 512 | if(ds.isSelected(osm)) { |
|---|
| 513 | lbl.setBackground(SystemColor.textHighlight); |
|---|
| 514 | lbl.setForeground(SystemColor.textHighlightText); |
|---|
| 515 | } else { |
|---|
| 516 | lbl.setBackground(SystemColor.control); |
|---|
| 517 | lbl.setForeground(SystemColor.controlText); |
|---|
| 518 | } |
|---|
| 519 | } |
|---|
| 520 | |
|---|
| 521 | /** |
|---|
| 522 | * Builds the labels with all necessary listeners for the info popup for the |
|---|
| 523 | * given OsmPrimitive |
|---|
| 524 | * @param osm The primitive to create the label for |
|---|
| 525 | * @return |
|---|
| 526 | */ |
|---|
| 527 | private final JLabel popupBuildPrimitiveLabels(final OsmPrimitive osm) { |
|---|
| 528 | final StringBuilder text = new StringBuilder(); |
|---|
| 529 | String name = osm.getDisplayName(DefaultNameFormatter.getInstance()); |
|---|
| 530 | if (osm.isNewOrUndeleted() || osm.isModified()) { |
|---|
| 531 | name = "<i><b>"+ name + "*</b></i>"; |
|---|
| 532 | } |
|---|
| 533 | text.append(name); |
|---|
| 534 | |
|---|
| 535 | boolean idShown = Main.pref.getBoolean("osm-primitives.showid"); |
|---|
| 536 | // fix #7557 - do not show ID twice |
|---|
| 537 | |
|---|
| 538 | if (!osm.isNew() && !idShown) { |
|---|
| 539 | text.append(" [id="+osm.getId()+"]"); |
|---|
| 540 | } |
|---|
| 541 | |
|---|
| 542 | if(osm.getUser() != null) { |
|---|
| 543 | text.append(" [" + tr("User:") + " " + osm.getUser().getName() + "]"); |
|---|
| 544 | } |
|---|
| 545 | |
|---|
| 546 | for (String key : osm.keySet()) { |
|---|
| 547 | text.append("<br>" + key + "=" + osm.get(key)); |
|---|
| 548 | } |
|---|
| 549 | |
|---|
| 550 | final JLabel l = new JLabel( |
|---|
| 551 | "<html>" +text.toString() + "</html>", |
|---|
| 552 | ImageProvider.get(osm.getDisplayType()), |
|---|
| 553 | JLabel.HORIZONTAL |
|---|
| 554 | ) { |
|---|
| 555 | // This is necessary so the label updates its colors when the |
|---|
| 556 | // selection is changed from the outside |
|---|
| 557 | @Override public void validate() { |
|---|
| 558 | super.validate(); |
|---|
| 559 | popupSetLabelColors(this, osm); |
|---|
| 560 | } |
|---|
| 561 | }; |
|---|
| 562 | l.setOpaque(true); |
|---|
| 563 | popupSetLabelColors(l, osm); |
|---|
| 564 | l.setFont(l.getFont().deriveFont(Font.PLAIN)); |
|---|
| 565 | l.setVerticalTextPosition(JLabel.TOP); |
|---|
| 566 | l.setHorizontalAlignment(JLabel.LEFT); |
|---|
| 567 | l.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); |
|---|
| 568 | l.addMouseListener(new MouseAdapter(){ |
|---|
| 569 | @Override public void mouseEntered(MouseEvent e) { |
|---|
| 570 | l.setBackground(SystemColor.info); |
|---|
| 571 | l.setForeground(SystemColor.infoText); |
|---|
| 572 | } |
|---|
| 573 | @Override public void mouseExited(MouseEvent e) { |
|---|
| 574 | popupSetLabelColors(l, osm); |
|---|
| 575 | } |
|---|
| 576 | @Override public void mouseClicked(MouseEvent e) { |
|---|
| 577 | DataSet ds = Main.main.getCurrentDataSet(); |
|---|
| 578 | // Let the user toggle the selection |
|---|
| 579 | ds.toggleSelected(osm); |
|---|
| 580 | l.validate(); |
|---|
| 581 | } |
|---|
| 582 | }); |
|---|
| 583 | // Sometimes the mouseEntered event is not catched, thus the label |
|---|
| 584 | // will not be highlighted, making it confusing. The MotionListener |
|---|
| 585 | // can correct this defect. |
|---|
| 586 | l.addMouseMotionListener(new MouseMotionListener() { |
|---|
| 587 | public void mouseMoved(MouseEvent e) { |
|---|
| 588 | l.setBackground(SystemColor.info); |
|---|
| 589 | l.setForeground(SystemColor.infoText); |
|---|
| 590 | } |
|---|
| 591 | public void mouseDragged(MouseEvent e) { |
|---|
| 592 | l.setBackground(SystemColor.info); |
|---|
| 593 | l.setForeground(SystemColor.infoText); |
|---|
| 594 | } |
|---|
| 595 | }); |
|---|
| 596 | return l; |
|---|
| 597 | } |
|---|
| 598 | } |
|---|
| 599 | |
|---|
| 600 | /** |
|---|
| 601 | * Everything, the collector is interested of. Access must be synchronized. |
|---|
| 602 | * @author imi |
|---|
| 603 | */ |
|---|
| 604 | static class MouseState { |
|---|
| 605 | Point mousePos; |
|---|
| 606 | int modifiers; |
|---|
| 607 | } |
|---|
| 608 | /** |
|---|
| 609 | * The last sent mouse movement event. |
|---|
| 610 | */ |
|---|
| 611 | MouseState mouseState = new MouseState(); |
|---|
| 612 | |
|---|
| 613 | private AWTEventListener awtListener = new AWTEventListener() { |
|---|
| 614 | public void eventDispatched(AWTEvent event) { |
|---|
| 615 | if (event instanceof InputEvent && |
|---|
| 616 | ((InputEvent)event).getComponent() == mv) { |
|---|
| 617 | synchronized (collector) { |
|---|
| 618 | mouseState.modifiers = ((InputEvent)event).getModifiersEx(); |
|---|
| 619 | if (event instanceof MouseEvent) { |
|---|
| 620 | mouseState.mousePos = ((MouseEvent)event).getPoint(); |
|---|
| 621 | } |
|---|
| 622 | collector.notify(); |
|---|
| 623 | } |
|---|
| 624 | } |
|---|
| 625 | } |
|---|
| 626 | }; |
|---|
| 627 | |
|---|
| 628 | private MouseMotionListener mouseMotionListener = new MouseMotionListener() { |
|---|
| 629 | public void mouseMoved(MouseEvent e) { |
|---|
| 630 | synchronized (collector) { |
|---|
| 631 | mouseState.modifiers = e.getModifiersEx(); |
|---|
| 632 | mouseState.mousePos = e.getPoint(); |
|---|
| 633 | collector.notify(); |
|---|
| 634 | } |
|---|
| 635 | } |
|---|
| 636 | |
|---|
| 637 | public void mouseDragged(MouseEvent e) { |
|---|
| 638 | mouseMoved(e); |
|---|
| 639 | } |
|---|
| 640 | }; |
|---|
| 641 | |
|---|
| 642 | private KeyAdapter keyAdapter = new KeyAdapter() { |
|---|
| 643 | @Override public void keyPressed(KeyEvent e) { |
|---|
| 644 | synchronized (collector) { |
|---|
| 645 | mouseState.modifiers = e.getModifiersEx(); |
|---|
| 646 | collector.notify(); |
|---|
| 647 | } |
|---|
| 648 | } |
|---|
| 649 | |
|---|
| 650 | @Override public void keyReleased(KeyEvent e) { |
|---|
| 651 | keyPressed(e); |
|---|
| 652 | } |
|---|
| 653 | }; |
|---|
| 654 | |
|---|
| 655 | private void registerListeners() { |
|---|
| 656 | // Listen to keyboard/mouse events for pressing/releasing alt key and |
|---|
| 657 | // inform the collector. |
|---|
| 658 | try { |
|---|
| 659 | Toolkit.getDefaultToolkit().addAWTEventListener(awtListener, |
|---|
| 660 | AWTEvent.KEY_EVENT_MASK | AWTEvent.MOUSE_EVENT_MASK | AWTEvent.MOUSE_MOTION_EVENT_MASK); |
|---|
| 661 | } catch (SecurityException ex) { |
|---|
| 662 | mv.addMouseMotionListener(mouseMotionListener); |
|---|
| 663 | mv.addKeyListener(keyAdapter); |
|---|
| 664 | } |
|---|
| 665 | } |
|---|
| 666 | |
|---|
| 667 | private void unregisterListeners() { |
|---|
| 668 | try { |
|---|
| 669 | Toolkit.getDefaultToolkit().removeAWTEventListener(awtListener); |
|---|
| 670 | } catch (SecurityException e) { |
|---|
| 671 | // Don't care, awtListener probably wasn't registered anyway |
|---|
| 672 | } |
|---|
| 673 | mv.removeMouseMotionListener(mouseMotionListener); |
|---|
| 674 | mv.removeKeyListener(keyAdapter); |
|---|
| 675 | } |
|---|
| 676 | |
|---|
| 677 | |
|---|
| 678 | /** |
|---|
| 679 | * Construct a new MapStatus and attach it to the map view. |
|---|
| 680 | * @param mapFrame The MapFrame the status line is part of. |
|---|
| 681 | */ |
|---|
| 682 | public MapStatus(final MapFrame mapFrame) { |
|---|
| 683 | this.mv = mapFrame.mapView; |
|---|
| 684 | this.collector = new Collector(mapFrame); |
|---|
| 685 | |
|---|
| 686 | lonText.addMouseListener(Main.main.menu.jumpToAct); |
|---|
| 687 | latText.addMouseListener(Main.main.menu.jumpToAct); |
|---|
| 688 | |
|---|
| 689 | // Listen for mouse movements and set the position text field |
|---|
| 690 | mv.addMouseMotionListener(new MouseMotionListener(){ |
|---|
| 691 | public void mouseDragged(MouseEvent e) { |
|---|
| 692 | mouseMoved(e); |
|---|
| 693 | } |
|---|
| 694 | public void mouseMoved(MouseEvent e) { |
|---|
| 695 | if (mv.center == null) |
|---|
| 696 | return; |
|---|
| 697 | // Do not update the view if ctrl is pressed. |
|---|
| 698 | if ((e.getModifiersEx() & MouseEvent.CTRL_DOWN_MASK) == 0) { |
|---|
| 699 | CoordinateFormat mCord = CoordinateFormat.getDefaultFormat(); |
|---|
| 700 | LatLon p = mv.getLatLon(e.getX(),e.getY()); |
|---|
| 701 | latText.setText(p.latToString(mCord)); |
|---|
| 702 | lonText.setText(p.lonToString(mCord)); |
|---|
| 703 | } |
|---|
| 704 | } |
|---|
| 705 | }); |
|---|
| 706 | |
|---|
| 707 | setLayout(new GridBagLayout()); |
|---|
| 708 | setBorder(BorderFactory.createEmptyBorder(5,5,5,5)); |
|---|
| 709 | |
|---|
| 710 | add(latText, GBC.std()); |
|---|
| 711 | add(lonText, GBC.std().insets(3,0,0,0)); |
|---|
| 712 | add(headingText, GBC.std().insets(3,0,0,0)); |
|---|
| 713 | add(angleText, GBC.std().insets(3,0,0,0)); |
|---|
| 714 | add(distText, GBC.std().insets(3,0,0,0)); |
|---|
| 715 | |
|---|
| 716 | helpText.setEditable(false); |
|---|
| 717 | add(nameText, GBC.std().insets(3,0,0,0)); |
|---|
| 718 | add(helpText, GBC.std().insets(3,0,0,0).fill(GBC.HORIZONTAL)); |
|---|
| 719 | |
|---|
| 720 | progressBar.setMaximum(PleaseWaitProgressMonitor.PROGRESS_BAR_MAX); |
|---|
| 721 | progressBar.setVisible(false); |
|---|
| 722 | GBC gbc = GBC.eol(); |
|---|
| 723 | gbc.ipadx = 100; |
|---|
| 724 | add(progressBar,gbc); |
|---|
| 725 | progressBar.addMouseListener(new MouseAdapter() { |
|---|
| 726 | @Override |
|---|
| 727 | public void mouseClicked(MouseEvent e) { |
|---|
| 728 | PleaseWaitProgressMonitor monitor = Main.currentProgressMonitor; |
|---|
| 729 | if (monitor != null) { |
|---|
| 730 | monitor.showForegroundDialog(); |
|---|
| 731 | } |
|---|
| 732 | } |
|---|
| 733 | }); |
|---|
| 734 | |
|---|
| 735 | // The background thread |
|---|
| 736 | thread = new Thread(collector, "Map Status Collector"); |
|---|
| 737 | thread.setDaemon(true); |
|---|
| 738 | thread.start(); |
|---|
| 739 | } |
|---|
| 740 | |
|---|
| 741 | public JPanel getAnglePanel() { |
|---|
| 742 | return angleText; |
|---|
| 743 | } |
|---|
| 744 | |
|---|
| 745 | public String helpTopic() { |
|---|
| 746 | return ht("/Statusline"); |
|---|
| 747 | } |
|---|
| 748 | |
|---|
| 749 | @Override |
|---|
| 750 | public synchronized void addMouseListener(MouseListener ml) { |
|---|
| 751 | //super.addMouseListener(ml); |
|---|
| 752 | lonText.addMouseListener(ml); |
|---|
| 753 | latText.addMouseListener(ml); |
|---|
| 754 | } |
|---|
| 755 | |
|---|
| 756 | public void setHelpText(String t) { |
|---|
| 757 | setHelpText(null, t); |
|---|
| 758 | } |
|---|
| 759 | public void setHelpText(Object id, String text) { |
|---|
| 760 | |
|---|
| 761 | StatusTextHistory entry = new StatusTextHistory(id, text); |
|---|
| 762 | |
|---|
| 763 | statusText.remove(entry); |
|---|
| 764 | statusText.add(entry); |
|---|
| 765 | |
|---|
| 766 | helpText.setText(text); |
|---|
| 767 | helpText.setToolTipText(text); |
|---|
| 768 | } |
|---|
| 769 | public void resetHelpText(Object id) { |
|---|
| 770 | if (statusText.isEmpty()) |
|---|
| 771 | return; |
|---|
| 772 | |
|---|
| 773 | StatusTextHistory entry = new StatusTextHistory(id, null); |
|---|
| 774 | if (statusText.get(statusText.size() - 1).equals(entry)) { |
|---|
| 775 | if (statusText.size() == 1) { |
|---|
| 776 | setHelpText(""); |
|---|
| 777 | } else { |
|---|
| 778 | StatusTextHistory history = statusText.get(statusText.size() - 2); |
|---|
| 779 | setHelpText(history.id, history.text); |
|---|
| 780 | } |
|---|
| 781 | } |
|---|
| 782 | statusText.remove(entry); |
|---|
| 783 | } |
|---|
| 784 | public void setAngle(double a) { |
|---|
| 785 | angleText.setText(a < 0 ? "--" : Math.round(a*10)/10.0 + " \u00B0"); |
|---|
| 786 | } |
|---|
| 787 | public void setHeading(double h) { |
|---|
| 788 | headingText.setText(h < 0 ? "--" : Math.round(h*10)/10.0 + " \u00B0"); |
|---|
| 789 | } |
|---|
| 790 | public void setDist(double dist) { |
|---|
| 791 | distText.setText(dist < 0 ? "--" : NavigatableComponent.getDistText(dist)); |
|---|
| 792 | } |
|---|
| 793 | public void activateAnglePanel(boolean activeFlag) { |
|---|
| 794 | angleText.setBackground(activeFlag ? ImageLabel.backColorActive : ImageLabel.backColor); |
|---|
| 795 | } |
|---|
| 796 | } |
|---|