001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.gui; 003 004import static org.openstreetmap.josm.data.osm.OsmPrimitive.isSelectablePredicate; 005import static org.openstreetmap.josm.data.osm.OsmPrimitive.isUsablePredicate; 006import static org.openstreetmap.josm.gui.help.HelpUtil.ht; 007import static org.openstreetmap.josm.tools.I18n.marktr; 008import static org.openstreetmap.josm.tools.I18n.tr; 009 010import java.awt.AWTEvent; 011import java.awt.Color; 012import java.awt.Component; 013import java.awt.Cursor; 014import java.awt.Dimension; 015import java.awt.EventQueue; 016import java.awt.Font; 017import java.awt.GridBagLayout; 018import java.awt.Point; 019import java.awt.SystemColor; 020import java.awt.Toolkit; 021import java.awt.event.AWTEventListener; 022import java.awt.event.ActionEvent; 023import java.awt.event.InputEvent; 024import java.awt.event.KeyAdapter; 025import java.awt.event.KeyEvent; 026import java.awt.event.MouseAdapter; 027import java.awt.event.MouseEvent; 028import java.awt.event.MouseListener; 029import java.awt.event.MouseMotionListener; 030import java.lang.reflect.InvocationTargetException; 031import java.text.DecimalFormat; 032import java.util.ArrayList; 033import java.util.Collection; 034import java.util.ConcurrentModificationException; 035import java.util.List; 036import java.util.Objects; 037import java.util.TreeSet; 038import java.util.concurrent.BlockingQueue; 039import java.util.concurrent.LinkedBlockingQueue; 040 041import javax.swing.AbstractAction; 042import javax.swing.BorderFactory; 043import javax.swing.JCheckBoxMenuItem; 044import javax.swing.JLabel; 045import javax.swing.JMenuItem; 046import javax.swing.JPanel; 047import javax.swing.JPopupMenu; 048import javax.swing.JProgressBar; 049import javax.swing.JScrollPane; 050import javax.swing.JSeparator; 051import javax.swing.Popup; 052import javax.swing.PopupFactory; 053import javax.swing.UIManager; 054import javax.swing.event.PopupMenuEvent; 055import javax.swing.event.PopupMenuListener; 056 057import org.openstreetmap.josm.Main; 058import org.openstreetmap.josm.data.Preferences.PreferenceChangeEvent; 059import org.openstreetmap.josm.data.Preferences.PreferenceChangedListener; 060import org.openstreetmap.josm.data.SystemOfMeasurement; 061import org.openstreetmap.josm.data.SystemOfMeasurement.SoMChangeListener; 062import org.openstreetmap.josm.data.coor.CoordinateFormat; 063import org.openstreetmap.josm.data.coor.LatLon; 064import org.openstreetmap.josm.data.osm.DataSet; 065import org.openstreetmap.josm.data.osm.OsmPrimitive; 066import org.openstreetmap.josm.data.osm.Way; 067import org.openstreetmap.josm.data.preferences.ColorProperty; 068import org.openstreetmap.josm.gui.help.Helpful; 069import org.openstreetmap.josm.gui.preferences.projection.ProjectionPreference; 070import org.openstreetmap.josm.gui.progress.PleaseWaitProgressMonitor; 071import org.openstreetmap.josm.gui.progress.PleaseWaitProgressMonitor.ProgressMonitorDialog; 072import org.openstreetmap.josm.gui.util.GuiHelper; 073import org.openstreetmap.josm.gui.widgets.ImageLabel; 074import org.openstreetmap.josm.gui.widgets.JosmTextField; 075import org.openstreetmap.josm.tools.Destroyable; 076import org.openstreetmap.josm.tools.GBC; 077import org.openstreetmap.josm.tools.ImageProvider; 078import org.openstreetmap.josm.tools.Predicate; 079 080/** 081 * A component that manages some status information display about the map. 082 * It keeps a status line below the map up to date and displays some tooltip 083 * information if the user hold the mouse long enough at some point. 084 * 085 * All this is done in background to not disturb other processes. 086 * 087 * The background thread does not alter any data of the map (read only thread). 088 * Also it is rather fail safe. In case of some error in the data, it just does 089 * nothing instead of whining and complaining. 090 * 091 * @author imi 092 */ 093public class MapStatus extends JPanel implements Helpful, Destroyable, PreferenceChangedListener { 094 095 private final DecimalFormat DECIMAL_FORMAT = new DecimalFormat(Main.pref.get("statusbar.decimal-format", "0.0")); 096 private final double DISTANCE_THRESHOLD = Main.pref.getDouble("statusbar.distance-threshold", 0.01); 097 098 /** 099 * Property for map status background color. 100 * @since 6789 101 */ 102 public static final ColorProperty PROP_BACKGROUND_COLOR = new ColorProperty( 103 marktr("Status bar background"), Color.decode("#b8cfe5")); 104 105 /** 106 * Property for map status background color (active state). 107 * @since 6789 108 */ 109 public static final ColorProperty PROP_ACTIVE_BACKGROUND_COLOR = new ColorProperty( 110 marktr("Status bar background: active"), Color.decode("#aaff5e")); 111 112 /** 113 * Property for map status foreground color. 114 * @since 6789 115 */ 116 public static final ColorProperty PROP_FOREGROUND_COLOR = new ColorProperty( 117 marktr("Status bar foreground"), Color.black); 118 119 /** 120 * Property for map status foreground color (active state). 121 * @since 6789 122 */ 123 public static final ColorProperty PROP_ACTIVE_FOREGROUND_COLOR = new ColorProperty( 124 marktr("Status bar foreground: active"), Color.black); 125 126 /** 127 * The MapView this status belongs to. 128 */ 129 private final MapView mv; 130 private final transient Collector collector; 131 132 public class BackgroundProgressMonitor implements ProgressMonitorDialog { 133 134 private String title; 135 private String customText; 136 137 private void updateText() { 138 if (customText != null && !customText.isEmpty()) { 139 progressBar.setToolTipText(tr("{0} ({1})", title, customText)); 140 } else { 141 progressBar.setToolTipText(title); 142 } 143 } 144 145 @Override 146 public void setVisible(boolean visible) { 147 progressBar.setVisible(visible); 148 } 149 150 @Override 151 public void updateProgress(int progress) { 152 progressBar.setValue(progress); 153 progressBar.repaint(); 154 MapStatus.this.doLayout(); 155 } 156 157 @Override 158 public void setCustomText(String text) { 159 this.customText = text; 160 updateText(); 161 } 162 163 @Override 164 public void setCurrentAction(String text) { 165 this.title = text; 166 updateText(); 167 } 168 169 @Override 170 public void setIndeterminate(boolean newValue) { 171 UIManager.put("ProgressBar.cycleTime", UIManager.getInt("ProgressBar.repaintInterval") * 100); 172 progressBar.setIndeterminate(newValue); 173 } 174 175 @Override 176 public void appendLogMessage(String message) { 177 if (message != null && !message.isEmpty()) { 178 Main.info("appendLogMessage not implemented for background tasks. Message was: " + message); 179 } 180 } 181 182 } 183 184 /** The {@link CoordinateFormat} set in the previous update */ 185 private transient CoordinateFormat previousCoordinateFormat; 186 private final ImageLabel latText = new ImageLabel("lat", 187 null, LatLon.SOUTH_POLE.latToString(CoordinateFormat.DEGREES_MINUTES_SECONDS).length(), PROP_BACKGROUND_COLOR.get()); 188 private final ImageLabel lonText = new ImageLabel("lon", 189 null, new LatLon(0, 180).lonToString(CoordinateFormat.DEGREES_MINUTES_SECONDS).length(), PROP_BACKGROUND_COLOR.get()); 190 private final ImageLabel headingText = new ImageLabel("heading", 191 tr("The (compass) heading of the line segment being drawn."), 192 DECIMAL_FORMAT.format(360).length() + 1, PROP_BACKGROUND_COLOR.get()); 193 private final ImageLabel angleText = new ImageLabel("angle", 194 tr("The angle between the previous and the current way segment."), 195 DECIMAL_FORMAT.format(360).length() + 1, PROP_BACKGROUND_COLOR.get()); 196 private final ImageLabel distText = new ImageLabel("dist", 197 tr("The length of the new way segment being drawn."), 10, PROP_BACKGROUND_COLOR.get()); 198 private final ImageLabel nameText = new ImageLabel("name", 199 tr("The name of the object at the mouse pointer."), 20, PROP_BACKGROUND_COLOR.get()); 200 private final JosmTextField helpText = new JosmTextField(); 201 private final JProgressBar progressBar = new JProgressBar(); 202 public final transient BackgroundProgressMonitor progressMonitor = new BackgroundProgressMonitor(); 203 204 private final transient SoMChangeListener somListener; 205 206 // Distance value displayed in distText, stored if refresh needed after a change of system of measurement 207 private double distValue; 208 209 // Determines if angle panel is enabled or not 210 private boolean angleEnabled; 211 212 /** 213 * This is the thread that runs in the background and collects the information displayed. 214 * It gets destroyed by destroy() when the MapFrame itself is destroyed. 215 */ 216 private final transient Thread thread; 217 218 private final transient List<StatusTextHistory> statusText = new ArrayList<>(); 219 220 private static class StatusTextHistory { 221 private final Object id; 222 private final String text; 223 224 StatusTextHistory(Object id, String text) { 225 this.id = id; 226 this.text = text; 227 } 228 229 @Override 230 public boolean equals(Object obj) { 231 return obj instanceof StatusTextHistory && ((StatusTextHistory) obj).id == id; 232 } 233 234 @Override 235 public int hashCode() { 236 return System.identityHashCode(id); 237 } 238 } 239 240 /** 241 * The collector class that waits for notification and then update the display objects. 242 * 243 * @author imi 244 */ 245 private final class Collector implements Runnable { 246 private final class CollectorWorker implements Runnable { 247 private final MouseState ms; 248 249 private CollectorWorker(MouseState ms) { 250 this.ms = ms; 251 } 252 253 @Override 254 public void run() { 255 // Freeze display when holding down CTRL 256 if ((ms.modifiers & MouseEvent.CTRL_DOWN_MASK) != 0) { 257 // update the information popup's labels though, because the selection might have changed from the outside 258 popupUpdateLabels(); 259 return; 260 } 261 262 // This try/catch is a hack to stop the flooding bug reports about this. 263 // The exception needed to handle with in the first place, means that this 264 // access to the data need to be restarted, if the main thread modifies the data. 265 DataSet ds = null; 266 // The popup != null check is required because a left-click produces several events as well, 267 // which would make this variable true. Of course we only want the popup to show 268 // if the middle mouse button has been pressed in the first place 269 boolean mouseNotMoved = oldMousePos != null 270 && oldMousePos.equals(ms.mousePos); 271 boolean isAtOldPosition = mouseNotMoved && popup != null; 272 boolean middleMouseDown = (ms.modifiers & MouseEvent.BUTTON2_DOWN_MASK) != 0; 273 try { 274 ds = mv.getCurrentDataSet(); 275 if (ds != null) { 276 // This is not perfect, if current dataset was changed during execution, the lock would be useless 277 if (isAtOldPosition && middleMouseDown) { 278 // Write lock is necessary when selecting in popupCycleSelection 279 // locks can not be upgraded -> if do read lock here and write lock later 280 // (in OsmPrimitive.updateFlags) then always occurs deadlock (#5814) 281 ds.beginUpdate(); 282 } else { 283 ds.getReadLock().lock(); 284 } 285 } 286 287 // Set the text label in the bottom status bar 288 // "if mouse moved only" was added to stop heap growing 289 if (!mouseNotMoved) { 290 statusBarElementUpdate(ms); 291 } 292 293 // Popup Information 294 // display them if the middle mouse button is pressed and keep them until the mouse is moved 295 if (middleMouseDown || isAtOldPosition) { 296 Collection<OsmPrimitive> osms = mv.getAllNearest(ms.mousePos, new Predicate<OsmPrimitive>() { 297 @Override 298 public boolean evaluate(OsmPrimitive o) { 299 return isUsablePredicate.evaluate(o) && isSelectablePredicate.evaluate(o); 300 } 301 }); 302 303 final JPanel c = new JPanel(new GridBagLayout()); 304 final JLabel lbl = new JLabel( 305 "<html>"+tr("Middle click again to cycle through.<br>"+ 306 "Hold CTRL to select directly from this list with the mouse.<hr>")+"</html>", 307 null, 308 JLabel.HORIZONTAL 309 ); 310 lbl.setHorizontalAlignment(JLabel.LEFT); 311 c.add(lbl, GBC.eol().insets(2, 0, 2, 0)); 312 313 // Only cycle if the mouse has not been moved and the middle mouse button has been pressed at least 314 // twice (the reason for this is the popup != null check for isAtOldPosition, see above. 315 // This is a nice side effect though, because it does not change selection of the first middle click) 316 if (isAtOldPosition && middleMouseDown) { 317 // Hand down mouse modifiers so the SHIFT mod can be handled correctly (see function) 318 popupCycleSelection(osms, ms.modifiers); 319 } 320 321 // These labels may need to be updated from the outside so collect them 322 List<JLabel> lbls = new ArrayList<>(osms.size()); 323 for (final OsmPrimitive osm : osms) { 324 JLabel l = popupBuildPrimitiveLabels(osm); 325 lbls.add(l); 326 c.add(l, GBC.eol().fill(GBC.HORIZONTAL).insets(2, 0, 2, 2)); 327 } 328 329 popupShowPopup(popupCreatePopup(c, ms), lbls); 330 } else { 331 popupHidePopup(); 332 } 333 334 oldMousePos = ms.mousePos; 335 } catch (ConcurrentModificationException x) { 336 Main.warn(x); 337 } finally { 338 if (ds != null) { 339 if (isAtOldPosition && middleMouseDown) { 340 ds.endUpdate(); 341 } else { 342 ds.getReadLock().unlock(); 343 } 344 } 345 } 346 } 347 } 348 349 /** 350 * the mouse position of the previous iteration. This is used to show 351 * the popup until the cursor is moved. 352 */ 353 private Point oldMousePos; 354 /** 355 * Contains the labels that are currently shown in the information 356 * popup 357 */ 358 private List<JLabel> popupLabels; 359 /** 360 * The popup displayed to show additional information 361 */ 362 private Popup popup; 363 364 private final MapFrame parent; 365 366 private final BlockingQueue<MouseState> incomingMouseState = new LinkedBlockingQueue<>(); 367 368 private Point lastMousePos; 369 370 Collector(MapFrame parent) { 371 this.parent = parent; 372 } 373 374 /** 375 * Execution function for the Collector. 376 */ 377 @Override 378 public void run() { 379 registerListeners(); 380 try { 381 for (;;) { 382 try { 383 final MouseState ms = incomingMouseState.take(); 384 if (parent != Main.map) 385 return; // exit, if new parent. 386 387 // Do nothing, if required data is missing 388 if (ms.mousePos == null || mv.center == null) { 389 continue; 390 } 391 392 EventQueue.invokeAndWait(new CollectorWorker(ms)); 393 } catch (InterruptedException e) { 394 // Occurs frequently during JOSM shutdown, log set to trace only 395 Main.trace("InterruptedException in "+MapStatus.class.getSimpleName()); 396 } catch (InvocationTargetException e) { 397 Main.warn(e); 398 } 399 } 400 } finally { 401 unregisterListeners(); 402 } 403 } 404 405 /** 406 * Creates a popup for the given content next to the cursor. Tries to 407 * keep the popup on screen and shows a vertical scrollbar, if the 408 * screen is too small. 409 * @param content popup content 410 * @param ms mouse state 411 * @return popup 412 */ 413 private Popup popupCreatePopup(Component content, MouseState ms) { 414 Point p = mv.getLocationOnScreen(); 415 Dimension scrn = GuiHelper.getScreenSize(); 416 417 // Create a JScrollPane around the content, in case there's not enough space 418 JScrollPane sp = GuiHelper.embedInVerticalScrollPane(content); 419 sp.setBorder(BorderFactory.createRaisedBevelBorder()); 420 // Implement max-size content-independent 421 Dimension prefsize = sp.getPreferredSize(); 422 int w = Math.min(prefsize.width, Math.min(800, (scrn.width/2) - 16)); 423 int h = Math.min(prefsize.height, scrn.height - 10); 424 sp.setPreferredSize(new Dimension(w, h)); 425 426 int xPos = p.x + ms.mousePos.x + 16; 427 // Display the popup to the left of the cursor if it would be cut 428 // off on its right, but only if more space is available 429 if (xPos + w > scrn.width && xPos > scrn.width/2) { 430 xPos = p.x + ms.mousePos.x - 4 - w; 431 } 432 int yPos = p.y + ms.mousePos.y + 16; 433 // Move the popup up if it would be cut off at its bottom but do not 434 // move it off screen on the top 435 if (yPos + h > scrn.height - 5) { 436 yPos = Math.max(5, scrn.height - h - 5); 437 } 438 439 PopupFactory pf = PopupFactory.getSharedInstance(); 440 return pf.getPopup(mv, sp, xPos, yPos); 441 } 442 443 /** 444 * Calls this to update the element that is shown in the statusbar 445 * @param ms mouse state 446 */ 447 private void statusBarElementUpdate(MouseState ms) { 448 final OsmPrimitive osmNearest = mv.getNearestNodeOrWay(ms.mousePos, isUsablePredicate, false); 449 if (osmNearest != null) { 450 nameText.setText(osmNearest.getDisplayName(DefaultNameFormatter.getInstance())); 451 } else { 452 nameText.setText(tr("(no object)")); 453 } 454 } 455 456 /** 457 * Call this with a set of primitives to cycle through them. Method 458 * will automatically select the next item and update the map 459 * @param osms primitives to cycle through 460 * @param mods modifiers (i.e. control keys) 461 */ 462 private void popupCycleSelection(Collection<OsmPrimitive> osms, int mods) { 463 DataSet ds = Main.main.getCurrentDataSet(); 464 // Find some items that are required for cycling through 465 OsmPrimitive firstItem = null; 466 OsmPrimitive firstSelected = null; 467 OsmPrimitive nextSelected = null; 468 for (final OsmPrimitive osm : osms) { 469 if (firstItem == null) { 470 firstItem = osm; 471 } 472 if (firstSelected != null && nextSelected == null) { 473 nextSelected = osm; 474 } 475 if (firstSelected == null && ds.isSelected(osm)) { 476 firstSelected = osm; 477 } 478 } 479 480 // Clear previous selection if SHIFT (add to selection) is not 481 // pressed. Cannot use "setSelected()" because it will cause a 482 // fireSelectionChanged event which is unnecessary at this point. 483 if ((mods & MouseEvent.SHIFT_DOWN_MASK) == 0) { 484 ds.clearSelection(); 485 } 486 487 // This will cycle through the available items. 488 if (firstSelected != null) { 489 ds.clearSelection(firstSelected); 490 if (nextSelected != null) { 491 ds.addSelected(nextSelected); 492 } 493 } else if (firstItem != null) { 494 ds.addSelected(firstItem); 495 } 496 } 497 498 /** 499 * Tries to hide the given popup 500 */ 501 private void popupHidePopup() { 502 popupLabels = null; 503 if (popup == null) 504 return; 505 final Popup staticPopup = popup; 506 popup = null; 507 EventQueue.invokeLater(new Runnable() { 508 @Override 509 public void run() { 510 staticPopup.hide(); 511 } 512 }); 513 } 514 515 /** 516 * Tries to show the given popup, can be hidden using {@link #popupHidePopup} 517 * If an old popup exists, it will be automatically hidden 518 * @param newPopup popup to show 519 * @param lbls lables to show (see {@link #popupLabels}) 520 */ 521 private void popupShowPopup(Popup newPopup, List<JLabel> lbls) { 522 final Popup staticPopup = newPopup; 523 if (this.popup != null) { 524 // If an old popup exists, remove it when the new popup has been drawn to keep flickering to a minimum 525 final Popup staticOldPopup = this.popup; 526 EventQueue.invokeLater(new Runnable() { 527 @Override 528 public void run() { 529 staticPopup.show(); 530 staticOldPopup.hide(); 531 } 532 }); 533 } else { 534 // There is no old popup 535 EventQueue.invokeLater(new Runnable() { 536 @Override 537 public void run() { 538 staticPopup.show(); 539 } 540 }); 541 } 542 this.popupLabels = lbls; 543 this.popup = newPopup; 544 } 545 546 /** 547 * This method should be called if the selection may have changed from 548 * outside of this class. This is the case when CTRL is pressed and the 549 * user clicks on the map instead of the popup. 550 */ 551 private void popupUpdateLabels() { 552 if (this.popup == null || this.popupLabels == null) 553 return; 554 for (JLabel l : this.popupLabels) { 555 l.validate(); 556 } 557 } 558 559 /** 560 * Sets the colors for the given label depending on the selected status of 561 * the given OsmPrimitive 562 * 563 * @param lbl The label to color 564 * @param osm The primitive to derive the colors from 565 */ 566 private void popupSetLabelColors(JLabel lbl, OsmPrimitive osm) { 567 DataSet ds = Main.main.getCurrentDataSet(); 568 if (ds.isSelected(osm)) { 569 lbl.setBackground(SystemColor.textHighlight); 570 lbl.setForeground(SystemColor.textHighlightText); 571 } else { 572 lbl.setBackground(SystemColor.control); 573 lbl.setForeground(SystemColor.controlText); 574 } 575 } 576 577 /** 578 * Builds the labels with all necessary listeners for the info popup for the 579 * given OsmPrimitive 580 * @param osm The primitive to create the label for 581 * @return labels for info popup 582 */ 583 private JLabel popupBuildPrimitiveLabels(final OsmPrimitive osm) { 584 final StringBuilder text = new StringBuilder(32); 585 String name = osm.getDisplayName(DefaultNameFormatter.getInstance()); 586 if (osm.isNewOrUndeleted() || osm.isModified()) { 587 name = "<i><b>"+ name + "*</b></i>"; 588 } 589 text.append(name); 590 591 boolean idShown = Main.pref.getBoolean("osm-primitives.showid"); 592 // fix #7557 - do not show ID twice 593 594 if (!osm.isNew() && !idShown) { 595 text.append(" [id=").append(osm.getId()).append(']'); 596 } 597 598 if (osm.getUser() != null) { 599 text.append(" [").append(tr("User:")).append(' ').append(osm.getUser().getName()).append(']'); 600 } 601 602 for (String key : osm.keySet()) { 603 text.append("<br>").append(key).append('=').append(osm.get(key)); 604 } 605 606 final JLabel l = new JLabel( 607 "<html>" + text.toString() + "</html>", 608 ImageProvider.get(osm.getDisplayType()), 609 JLabel.HORIZONTAL 610 ) { 611 // This is necessary so the label updates its colors when the 612 // selection is changed from the outside 613 @Override 614 public void validate() { 615 super.validate(); 616 popupSetLabelColors(this, osm); 617 } 618 }; 619 l.setOpaque(true); 620 popupSetLabelColors(l, osm); 621 l.setFont(l.getFont().deriveFont(Font.PLAIN)); 622 l.setVerticalTextPosition(JLabel.TOP); 623 l.setHorizontalAlignment(JLabel.LEFT); 624 l.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); 625 l.addMouseListener(new MouseAdapter() { 626 @Override 627 public void mouseEntered(MouseEvent e) { 628 l.setBackground(SystemColor.info); 629 l.setForeground(SystemColor.infoText); 630 } 631 632 @Override 633 public void mouseExited(MouseEvent e) { 634 popupSetLabelColors(l, osm); 635 } 636 637 @Override 638 public void mouseClicked(MouseEvent e) { 639 DataSet ds = Main.main.getCurrentDataSet(); 640 // Let the user toggle the selection 641 ds.toggleSelected(osm); 642 l.validate(); 643 } 644 }); 645 // Sometimes the mouseEntered event is not catched, thus the label 646 // will not be highlighted, making it confusing. The MotionListener can correct this defect. 647 l.addMouseMotionListener(new MouseMotionListener() { 648 @Override 649 public void mouseMoved(MouseEvent e) { 650 l.setBackground(SystemColor.info); 651 l.setForeground(SystemColor.infoText); 652 } 653 654 @Override 655 public void mouseDragged(MouseEvent e) { 656 l.setBackground(SystemColor.info); 657 l.setForeground(SystemColor.infoText); 658 } 659 }); 660 return l; 661 } 662 663 /** 664 * Called whenever the mouse position or modifiers changed. 665 * @param mousePos The new mouse position. <code>null</code> if it did not change. 666 * @param modifiers The new modifiers. 667 */ 668 public synchronized void updateMousePosition(Point mousePos, int modifiers) { 669 if (mousePos != null) { 670 lastMousePos = mousePos; 671 } 672 MouseState ms = new MouseState(lastMousePos, modifiers); 673 // remove mouse states that are in the queue. Our mouse state is newer. 674 incomingMouseState.clear(); 675 if (!incomingMouseState.offer(ms)) { 676 Main.warn("Unable to handle new MouseState: " + ms); 677 } 678 } 679 } 680 681 /** 682 * Everything, the collector is interested of. Access must be synchronized. 683 * @author imi 684 */ 685 private static class MouseState { 686 private final Point mousePos; 687 private final int modifiers; 688 689 MouseState(Point mousePos, int modifiers) { 690 this.mousePos = mousePos; 691 this.modifiers = modifiers; 692 } 693 } 694 695 private final transient AWTEventListener awtListener = new AWTEventListener() { 696 @Override 697 public void eventDispatched(AWTEvent event) { 698 if (event instanceof InputEvent && 699 ((InputEvent) event).getComponent() == mv) { 700 synchronized (collector) { 701 int modifiers = ((InputEvent) event).getModifiersEx(); 702 Point mousePos = null; 703 if (event instanceof MouseEvent) { 704 mousePos = ((MouseEvent) event).getPoint(); 705 } 706 collector.updateMousePosition(mousePos, modifiers); 707 } 708 } 709 } 710 }; 711 712 private final transient MouseMotionListener mouseMotionListener = new MouseMotionListener() { 713 @Override 714 public void mouseMoved(MouseEvent e) { 715 synchronized (collector) { 716 collector.updateMousePosition(e.getPoint(), e.getModifiersEx()); 717 } 718 } 719 720 @Override 721 public void mouseDragged(MouseEvent e) { 722 mouseMoved(e); 723 } 724 }; 725 726 private final transient KeyAdapter keyAdapter = new KeyAdapter() { 727 @Override public void keyPressed(KeyEvent e) { 728 synchronized (collector) { 729 collector.updateMousePosition(null, e.getModifiersEx()); 730 } 731 } 732 733 @Override public void keyReleased(KeyEvent e) { 734 keyPressed(e); 735 } 736 }; 737 738 private void registerListeners() { 739 // Listen to keyboard/mouse events for pressing/releasing alt key and 740 // inform the collector. 741 try { 742 Toolkit.getDefaultToolkit().addAWTEventListener(awtListener, 743 AWTEvent.KEY_EVENT_MASK | AWTEvent.MOUSE_EVENT_MASK | AWTEvent.MOUSE_MOTION_EVENT_MASK); 744 } catch (SecurityException ex) { 745 mv.addMouseMotionListener(mouseMotionListener); 746 mv.addKeyListener(keyAdapter); 747 } 748 } 749 750 private void unregisterListeners() { 751 try { 752 Toolkit.getDefaultToolkit().removeAWTEventListener(awtListener); 753 } catch (SecurityException e) { 754 // Don't care, awtListener probably wasn't registered anyway 755 if (Main.isTraceEnabled()) { 756 Main.trace(e.getMessage()); 757 } 758 } 759 mv.removeMouseMotionListener(mouseMotionListener); 760 mv.removeKeyListener(keyAdapter); 761 } 762 763 private class MapStatusPopupMenu extends JPopupMenu { 764 765 private final JMenuItem jumpButton = add(Main.main.menu.jumpToAct); 766 767 /** Icons for selecting {@link SystemOfMeasurement} */ 768 private final Collection<JCheckBoxMenuItem> somItems = new ArrayList<>(); 769 /** Icons for selecting {@link CoordinateFormat} */ 770 private final Collection<JCheckBoxMenuItem> coordinateFormatItems = new ArrayList<>(); 771 772 private final JSeparator separator = new JSeparator(); 773 774 private final JMenuItem doNotHide = new JCheckBoxMenuItem(new AbstractAction(tr("Do not hide status bar")) { 775 @Override 776 public void actionPerformed(ActionEvent e) { 777 boolean sel = ((JCheckBoxMenuItem) e.getSource()).getState(); 778 Main.pref.put("statusbar.always-visible", sel); 779 } 780 }); 781 782 MapStatusPopupMenu() { 783 for (final String key : new TreeSet<>(SystemOfMeasurement.ALL_SYSTEMS.keySet())) { 784 JCheckBoxMenuItem item = new JCheckBoxMenuItem(new AbstractAction(key) { 785 @Override 786 public void actionPerformed(ActionEvent e) { 787 updateSystemOfMeasurement(key); 788 } 789 }); 790 somItems.add(item); 791 add(item); 792 } 793 for (final CoordinateFormat format : CoordinateFormat.values()) { 794 JCheckBoxMenuItem item = new JCheckBoxMenuItem(new AbstractAction(format.getDisplayName()) { 795 @Override 796 public void actionPerformed(ActionEvent e) { 797 CoordinateFormat.setCoordinateFormat(format); 798 } 799 }); 800 coordinateFormatItems.add(item); 801 add(item); 802 } 803 804 add(separator); 805 add(doNotHide); 806 807 addPopupMenuListener(new PopupMenuListener() { 808 @Override 809 public void popupMenuWillBecomeVisible(PopupMenuEvent e) { 810 Component invoker = ((JPopupMenu) e.getSource()).getInvoker(); 811 jumpButton.setVisible(latText.equals(invoker) || lonText.equals(invoker)); 812 String currentSOM = ProjectionPreference.PROP_SYSTEM_OF_MEASUREMENT.get(); 813 for (JMenuItem item : somItems) { 814 item.setSelected(item.getText().equals(currentSOM)); 815 item.setVisible(distText.equals(invoker)); 816 } 817 final String currentCorrdinateFormat = CoordinateFormat.getDefaultFormat().getDisplayName(); 818 for (JMenuItem item : coordinateFormatItems) { 819 item.setSelected(currentCorrdinateFormat.equals(item.getText())); 820 item.setVisible(latText.equals(invoker) || lonText.equals(invoker)); 821 } 822 separator.setVisible(distText.equals(invoker) || latText.equals(invoker) || lonText.equals(invoker)); 823 doNotHide.setSelected(Main.pref.getBoolean("statusbar.always-visible", true)); 824 } 825 826 @Override 827 public void popupMenuWillBecomeInvisible(PopupMenuEvent e) { 828 // Do nothing 829 } 830 831 @Override 832 public void popupMenuCanceled(PopupMenuEvent e) { 833 // Do nothing 834 } 835 }); 836 } 837 } 838 839 /** 840 * Construct a new MapStatus and attach it to the map view. 841 * @param mapFrame The MapFrame the status line is part of. 842 */ 843 public MapStatus(final MapFrame mapFrame) { 844 this.mv = mapFrame.mapView; 845 this.collector = new Collector(mapFrame); 846 847 // Context menu of status bar 848 setComponentPopupMenu(new MapStatusPopupMenu()); 849 850 // also show Jump To dialog on mouse click (except context menu) 851 MouseListener jumpToOnLeftClick = new MouseAdapter() { 852 @Override 853 public void mouseClicked(MouseEvent e) { 854 if (e.getButton() != MouseEvent.BUTTON3) { 855 Main.main.menu.jumpToAct.showJumpToDialog(); 856 } 857 } 858 }; 859 860 // Listen for mouse movements and set the position text field 861 mv.addMouseMotionListener(new MouseMotionListener() { 862 @Override 863 public void mouseDragged(MouseEvent e) { 864 mouseMoved(e); 865 } 866 867 @Override 868 public void mouseMoved(MouseEvent e) { 869 if (mv.center == null) 870 return; 871 // Do not update the view if ctrl is pressed. 872 if ((e.getModifiersEx() & MouseEvent.CTRL_DOWN_MASK) == 0) { 873 CoordinateFormat mCord = CoordinateFormat.getDefaultFormat(); 874 LatLon p = mv.getLatLon(e.getX(), e.getY()); 875 latText.setText(p.latToString(mCord)); 876 lonText.setText(p.lonToString(mCord)); 877 if (Objects.equals(previousCoordinateFormat, mCord)) { 878 // do nothing 879 } else if (CoordinateFormat.EAST_NORTH.equals(mCord)) { 880 latText.setIcon("northing"); 881 lonText.setIcon("easting"); 882 latText.setToolTipText(tr("The northing at the mouse pointer.")); 883 lonText.setToolTipText(tr("The easting at the mouse pointer.")); 884 previousCoordinateFormat = mCord; 885 } else { 886 latText.setIcon("lat"); 887 lonText.setIcon("lon"); 888 latText.setToolTipText(tr("The geographic latitude at the mouse pointer.")); 889 lonText.setToolTipText(tr("The geographic longitude at the mouse pointer.")); 890 previousCoordinateFormat = mCord; 891 } 892 } 893 } 894 }); 895 896 setLayout(new GridBagLayout()); 897 setBorder(BorderFactory.createEmptyBorder(1, 2, 1, 2)); 898 899 latText.setInheritsPopupMenu(true); 900 lonText.setInheritsPopupMenu(true); 901 headingText.setInheritsPopupMenu(true); 902 distText.setInheritsPopupMenu(true); 903 nameText.setInheritsPopupMenu(true); 904 905 add(latText, GBC.std()); 906 add(lonText, GBC.std().insets(3, 0, 0, 0)); 907 add(headingText, GBC.std().insets(3, 0, 0, 0)); 908 add(angleText, GBC.std().insets(3, 0, 0, 0)); 909 add(distText, GBC.std().insets(3, 0, 0, 0)); 910 911 if (Main.pref.getBoolean("statusbar.change-system-of-measurement-on-click", true)) { 912 distText.addMouseListener(new MouseAdapter() { 913 private final List<String> soms = new ArrayList<>(new TreeSet<>(SystemOfMeasurement.ALL_SYSTEMS.keySet())); 914 915 @Override 916 public void mouseClicked(MouseEvent e) { 917 if (!e.isPopupTrigger() && e.getButton() == MouseEvent.BUTTON1) { 918 String som = ProjectionPreference.PROP_SYSTEM_OF_MEASUREMENT.get(); 919 String newsom = soms.get((soms.indexOf(som)+1) % soms.size()); 920 updateSystemOfMeasurement(newsom); 921 } 922 } 923 }); 924 } 925 926 SystemOfMeasurement.addSoMChangeListener(somListener = new SoMChangeListener() { 927 @Override 928 public void systemOfMeasurementChanged(String oldSoM, String newSoM) { 929 setDist(distValue); 930 } 931 }); 932 933 latText.addMouseListener(jumpToOnLeftClick); 934 lonText.addMouseListener(jumpToOnLeftClick); 935 936 helpText.setEditable(false); 937 add(nameText, GBC.std().insets(3, 0, 0, 0)); 938 add(helpText, GBC.std().insets(3, 0, 0, 0).fill(GBC.HORIZONTAL)); 939 940 progressBar.setMaximum(PleaseWaitProgressMonitor.PROGRESS_BAR_MAX); 941 progressBar.setVisible(false); 942 GBC gbc = GBC.eol(); 943 gbc.ipadx = 100; 944 add(progressBar, gbc); 945 progressBar.addMouseListener(new MouseAdapter() { 946 @Override 947 public void mouseClicked(MouseEvent e) { 948 PleaseWaitProgressMonitor monitor = Main.currentProgressMonitor; 949 if (monitor != null) { 950 monitor.showForegroundDialog(); 951 } 952 } 953 }); 954 955 Main.pref.addPreferenceChangeListener(this); 956 957 // The background thread 958 thread = new Thread(collector, "Map Status Collector"); 959 thread.setDaemon(true); 960 thread.start(); 961 } 962 963 /** 964 * Updates the system of measurement and displays a notification. 965 * @param newsom The new system of measurement to set 966 * @since 6960 967 */ 968 public void updateSystemOfMeasurement(String newsom) { 969 SystemOfMeasurement.setSystemOfMeasurement(newsom); 970 if (Main.pref.getBoolean("statusbar.notify.change-system-of-measurement", true)) { 971 new Notification(tr("System of measurement changed to {0}", newsom)) 972 .setDuration(Notification.TIME_SHORT) 973 .show(); 974 } 975 } 976 977 public JPanel getAnglePanel() { 978 return angleText; 979 } 980 981 @Override 982 public String helpTopic() { 983 return ht("/StatusBar"); 984 } 985 986 @Override 987 public synchronized void addMouseListener(MouseListener ml) { 988 lonText.addMouseListener(ml); 989 latText.addMouseListener(ml); 990 } 991 992 public void setHelpText(String t) { 993 setHelpText(null, t); 994 } 995 996 public void setHelpText(Object id, final String text) { 997 998 StatusTextHistory entry = new StatusTextHistory(id, text); 999 1000 statusText.remove(entry); 1001 statusText.add(entry); 1002 1003 GuiHelper.runInEDT(new Runnable() { 1004 @Override 1005 public void run() { 1006 helpText.setText(text); 1007 helpText.setToolTipText(text); 1008 } 1009 }); 1010 } 1011 1012 public void resetHelpText(Object id) { 1013 if (statusText.isEmpty()) 1014 return; 1015 1016 StatusTextHistory entry = new StatusTextHistory(id, null); 1017 if (statusText.get(statusText.size() - 1).equals(entry)) { 1018 if (statusText.size() == 1) { 1019 setHelpText(""); 1020 } else { 1021 StatusTextHistory history = statusText.get(statusText.size() - 2); 1022 setHelpText(history.id, history.text); 1023 } 1024 } 1025 statusText.remove(entry); 1026 } 1027 1028 public void setAngle(double a) { 1029 angleText.setText(a < 0 ? "--" : DECIMAL_FORMAT.format(a) + " \u00B0"); 1030 } 1031 1032 public void setHeading(double h) { 1033 headingText.setText(h < 0 ? "--" : DECIMAL_FORMAT.format(h) + " \u00B0"); 1034 } 1035 1036 /** 1037 * Sets the distance text to the given value 1038 * @param dist The distance value to display, in meters 1039 */ 1040 public void setDist(double dist) { 1041 distValue = dist; 1042 distText.setText(dist < 0 ? "--" : NavigatableComponent.getDistText(dist, DECIMAL_FORMAT, DISTANCE_THRESHOLD)); 1043 } 1044 1045 /** 1046 * Sets the distance text to the total sum of given ways length 1047 * @param ways The ways to consider for the total distance 1048 * @since 5991 1049 */ 1050 public void setDist(Collection<Way> ways) { 1051 double dist = -1; 1052 // Compute total length of selected way(s) until an arbitrary limit set to 250 ways 1053 // in order to prevent performance issue if a large number of ways are selected (old behaviour kept in that case, see #8403) 1054 int maxWays = Math.max(1, Main.pref.getInteger("selection.max-ways-for-statusline", 250)); 1055 if (!ways.isEmpty() && ways.size() <= maxWays) { 1056 dist = 0.0; 1057 for (Way w : ways) { 1058 dist += w.getLength(); 1059 } 1060 } 1061 setDist(dist); 1062 } 1063 1064 /** 1065 * Activates the angle panel. 1066 * @param activeFlag {@code true} to activate it, {@code false} to deactivate it 1067 */ 1068 public void activateAnglePanel(boolean activeFlag) { 1069 angleEnabled = activeFlag; 1070 refreshAnglePanel(); 1071 } 1072 1073 private void refreshAnglePanel() { 1074 angleText.setBackground(angleEnabled ? PROP_ACTIVE_BACKGROUND_COLOR.get() : PROP_BACKGROUND_COLOR.get()); 1075 angleText.setForeground(angleEnabled ? PROP_ACTIVE_FOREGROUND_COLOR.get() : PROP_FOREGROUND_COLOR.get()); 1076 } 1077 1078 @Override 1079 public void destroy() { 1080 SystemOfMeasurement.removeSoMChangeListener(somListener); 1081 Main.pref.removePreferenceChangeListener(this); 1082 1083 // MapFrame gets destroyed when the last layer is removed, but the status line background 1084 // thread that collects the information doesn't get destroyed automatically. 1085 if (thread != null) { 1086 try { 1087 thread.interrupt(); 1088 } catch (Exception e) { 1089 Main.error(e); 1090 } 1091 } 1092 } 1093 1094 @Override 1095 public void preferenceChanged(PreferenceChangeEvent e) { 1096 String key = e.getKey(); 1097 if (key.startsWith("color.")) { 1098 key = key.substring("color.".length()); 1099 if (PROP_BACKGROUND_COLOR.getKey().equals(key) || PROP_FOREGROUND_COLOR.getKey().equals(key)) { 1100 for (ImageLabel il : new ImageLabel[]{latText, lonText, headingText, distText, nameText}) { 1101 il.setBackground(PROP_BACKGROUND_COLOR.get()); 1102 il.setForeground(PROP_FOREGROUND_COLOR.get()); 1103 } 1104 refreshAnglePanel(); 1105 } else if (PROP_ACTIVE_BACKGROUND_COLOR.getKey().equals(key) || PROP_ACTIVE_FOREGROUND_COLOR.getKey().equals(key)) { 1106 refreshAnglePanel(); 1107 } 1108 } 1109 } 1110 1111 /** 1112 * Loads all colors from preferences. 1113 * @since 6789 1114 */ 1115 public static void getColors() { 1116 PROP_BACKGROUND_COLOR.get(); 1117 PROP_FOREGROUND_COLOR.get(); 1118 PROP_ACTIVE_BACKGROUND_COLOR.get(); 1119 PROP_ACTIVE_FOREGROUND_COLOR.get(); 1120 } 1121}