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.ComponentAdapter; 024import java.awt.event.ComponentEvent; 025import java.awt.event.InputEvent; 026import java.awt.event.KeyAdapter; 027import java.awt.event.KeyEvent; 028import java.awt.event.MouseAdapter; 029import java.awt.event.MouseEvent; 030import java.awt.event.MouseListener; 031import java.awt.event.MouseMotionListener; 032import java.lang.reflect.InvocationTargetException; 033import java.text.DecimalFormat; 034import java.util.ArrayList; 035import java.util.Collection; 036import java.util.ConcurrentModificationException; 037import java.util.List; 038import java.util.Objects; 039import java.util.TreeSet; 040import java.util.concurrent.BlockingQueue; 041import java.util.concurrent.LinkedBlockingQueue; 042 043import javax.swing.AbstractAction; 044import javax.swing.BorderFactory; 045import javax.swing.JCheckBoxMenuItem; 046import javax.swing.JLabel; 047import javax.swing.JMenuItem; 048import javax.swing.JPanel; 049import javax.swing.JPopupMenu; 050import javax.swing.JProgressBar; 051import javax.swing.JScrollPane; 052import javax.swing.JSeparator; 053import javax.swing.Popup; 054import javax.swing.PopupFactory; 055import javax.swing.UIManager; 056import javax.swing.event.PopupMenuEvent; 057import javax.swing.event.PopupMenuListener; 058 059import org.openstreetmap.josm.Main; 060import org.openstreetmap.josm.data.Preferences.PreferenceChangeEvent; 061import org.openstreetmap.josm.data.Preferences.PreferenceChangedListener; 062import org.openstreetmap.josm.data.SystemOfMeasurement; 063import org.openstreetmap.josm.data.SystemOfMeasurement.SoMChangeListener; 064import org.openstreetmap.josm.data.coor.CoordinateFormat; 065import org.openstreetmap.josm.data.coor.LatLon; 066import org.openstreetmap.josm.data.osm.DataSet; 067import org.openstreetmap.josm.data.osm.OsmPrimitive; 068import org.openstreetmap.josm.data.osm.Way; 069import org.openstreetmap.josm.data.preferences.ColorProperty; 070import org.openstreetmap.josm.gui.help.Helpful; 071import org.openstreetmap.josm.gui.preferences.projection.ProjectionPreference; 072import org.openstreetmap.josm.gui.progress.PleaseWaitProgressMonitor; 073import org.openstreetmap.josm.gui.progress.PleaseWaitProgressMonitor.ProgressMonitorDialog; 074import org.openstreetmap.josm.gui.util.GuiHelper; 075import org.openstreetmap.josm.gui.widgets.ImageLabel; 076import org.openstreetmap.josm.gui.widgets.JosmTextField; 077import org.openstreetmap.josm.tools.Destroyable; 078import org.openstreetmap.josm.tools.GBC; 079import org.openstreetmap.josm.tools.ImageProvider; 080import org.openstreetmap.josm.tools.Predicate; 081 082/** 083 * A component that manages some status information display about the map. 084 * It keeps a status line below the map up to date and displays some tooltip 085 * information if the user hold the mouse long enough at some point. 086 * 087 * All this is done in background to not disturb other processes. 088 * 089 * The background thread does not alter any data of the map (read only thread). 090 * Also it is rather fail safe. In case of some error in the data, it just does 091 * nothing instead of whining and complaining. 092 * 093 * @author imi 094 */ 095public final class MapStatus extends JPanel implements Helpful, Destroyable, PreferenceChangedListener, SoMChangeListener { 096 097 private final DecimalFormat DECIMAL_FORMAT = new DecimalFormat(Main.pref.get("statusbar.decimal-format", "0.0")); 098 private final double DISTANCE_THRESHOLD = Main.pref.getDouble("statusbar.distance-threshold", 0.01); 099 100 /** 101 * Property for map status background color. 102 * @since 6789 103 */ 104 public static final ColorProperty PROP_BACKGROUND_COLOR = new ColorProperty( 105 marktr("Status bar background"), Color.decode("#b8cfe5")); 106 107 /** 108 * Property for map status background color (active state). 109 * @since 6789 110 */ 111 public static final ColorProperty PROP_ACTIVE_BACKGROUND_COLOR = new ColorProperty( 112 marktr("Status bar background: active"), Color.decode("#aaff5e")); 113 114 /** 115 * Property for map status foreground color. 116 * @since 6789 117 */ 118 public static final ColorProperty PROP_FOREGROUND_COLOR = new ColorProperty( 119 marktr("Status bar foreground"), Color.black); 120 121 /** 122 * Property for map status foreground color (active state). 123 * @since 6789 124 */ 125 public static final ColorProperty PROP_ACTIVE_FOREGROUND_COLOR = new ColorProperty( 126 marktr("Status bar foreground: active"), Color.black); 127 128 /** 129 * The MapView this status belongs to. 130 */ 131 private final MapView mv; 132 private final transient Collector collector; 133 134 public class BackgroundProgressMonitor implements ProgressMonitorDialog { 135 136 private String title; 137 private String customText; 138 139 private void updateText() { 140 if (customText != null && !customText.isEmpty()) { 141 progressBar.setToolTipText(tr("{0} ({1})", title, customText)); 142 } else { 143 progressBar.setToolTipText(title); 144 } 145 } 146 147 @Override 148 public void setVisible(boolean visible) { 149 progressBar.setVisible(visible); 150 } 151 152 @Override 153 public void updateProgress(int progress) { 154 progressBar.setValue(progress); 155 progressBar.repaint(); 156 MapStatus.this.doLayout(); 157 } 158 159 @Override 160 public void setCustomText(String text) { 161 this.customText = text; 162 updateText(); 163 } 164 165 @Override 166 public void setCurrentAction(String text) { 167 this.title = text; 168 updateText(); 169 } 170 171 @Override 172 public void setIndeterminate(boolean newValue) { 173 UIManager.put("ProgressBar.cycleTime", UIManager.getInt("ProgressBar.repaintInterval") * 100); 174 progressBar.setIndeterminate(newValue); 175 } 176 177 @Override 178 public void appendLogMessage(String message) { 179 if (message != null && !message.isEmpty()) { 180 Main.info("appendLogMessage not implemented for background tasks. Message was: " + message); 181 } 182 } 183 184 } 185 186 /** The {@link CoordinateFormat} set in the previous update */ 187 private transient CoordinateFormat previousCoordinateFormat; 188 private final ImageLabel latText = new ImageLabel("lat", 189 null, LatLon.SOUTH_POLE.latToString(CoordinateFormat.DEGREES_MINUTES_SECONDS).length(), PROP_BACKGROUND_COLOR.get()); 190 private final ImageLabel lonText = new ImageLabel("lon", 191 null, new LatLon(0, 180).lonToString(CoordinateFormat.DEGREES_MINUTES_SECONDS).length(), PROP_BACKGROUND_COLOR.get()); 192 private final ImageLabel headingText = new ImageLabel("heading", 193 tr("The (compass) heading of the line segment being drawn."), 194 DECIMAL_FORMAT.format(360).length() + 1, PROP_BACKGROUND_COLOR.get()); 195 private final ImageLabel angleText = new ImageLabel("angle", 196 tr("The angle between the previous and the current way segment."), 197 DECIMAL_FORMAT.format(360).length() + 1, PROP_BACKGROUND_COLOR.get()); 198 private final ImageLabel distText = new ImageLabel("dist", 199 tr("The length of the new way segment being drawn."), 10, PROP_BACKGROUND_COLOR.get()); 200 private final ImageLabel nameText = new ImageLabel("name", 201 tr("The name of the object at the mouse pointer."), getNameLabelCharacterCount(Main.parent), PROP_BACKGROUND_COLOR.get()); 202 private final JosmTextField helpText = new JosmTextField(); 203 private final JProgressBar progressBar = new JProgressBar(); 204 private final transient ComponentAdapter mvComponentAdapter; 205 public final transient BackgroundProgressMonitor progressMonitor = new BackgroundProgressMonitor(); 206 207 // Distance value displayed in distText, stored if refresh needed after a change of system of measurement 208 private double distValue; 209 210 // Determines if angle panel is enabled or not 211 private boolean angleEnabled; 212 213 /** 214 * This is the thread that runs in the background and collects the information displayed. 215 * It gets destroyed by destroy() when the MapFrame itself is destroyed. 216 */ 217 private final transient Thread thread; 218 219 private final transient List<StatusTextHistory> statusText = new ArrayList<>(); 220 221 private static class StatusTextHistory { 222 private final Object id; 223 private final String text; 224 225 StatusTextHistory(Object id, String text) { 226 this.id = id; 227 this.text = text; 228 } 229 230 @Override 231 public boolean equals(Object obj) { 232 return obj instanceof StatusTextHistory && ((StatusTextHistory) obj).id == id; 233 } 234 235 @Override 236 public int hashCode() { 237 return System.identityHashCode(id); 238 } 239 } 240 241 /** 242 * The collector class that waits for notification and then update the display objects. 243 * 244 * @author imi 245 */ 246 private final class Collector implements Runnable { 247 private final class CollectorWorker implements Runnable { 248 private final MouseState ms; 249 250 private CollectorWorker(MouseState ms) { 251 this.ms = ms; 252 } 253 254 @Override 255 public void run() { 256 // Freeze display when holding down CTRL 257 if ((ms.modifiers & MouseEvent.CTRL_DOWN_MASK) != 0) { 258 // update the information popup's labels though, because the selection might have changed from the outside 259 popupUpdateLabels(); 260 return; 261 } 262 263 // This try/catch is a hack to stop the flooding bug reports about this. 264 // The exception needed to handle with in the first place, means that this 265 // access to the data need to be restarted, if the main thread modifies the data. 266 DataSet ds = null; 267 // The popup != null check is required because a left-click produces several events as well, 268 // which would make this variable true. Of course we only want the popup to show 269 // if the middle mouse button has been pressed in the first place 270 boolean mouseNotMoved = oldMousePos != null && 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.getLayerManager().getEditDataSet(); 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.getCenter() == 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.getLayerManager().getEditDataSet(); 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.getLayerManager().getEditDataSet(); 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.getLayerManager().getEditDataSet(); 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.getCenter() == 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(this); 927 928 latText.addMouseListener(jumpToOnLeftClick); 929 lonText.addMouseListener(jumpToOnLeftClick); 930 931 helpText.setEditable(false); 932 add(nameText, GBC.std().insets(3, 0, 0, 0)); 933 add(helpText, GBC.std().insets(3, 0, 0, 0).fill(GBC.HORIZONTAL)); 934 935 progressBar.setMaximum(PleaseWaitProgressMonitor.PROGRESS_BAR_MAX); 936 progressBar.setVisible(false); 937 GBC gbc = GBC.eol(); 938 gbc.ipadx = 100; 939 add(progressBar, gbc); 940 progressBar.addMouseListener(new MouseAdapter() { 941 @Override 942 public void mouseClicked(MouseEvent e) { 943 PleaseWaitProgressMonitor monitor = Main.currentProgressMonitor; 944 if (monitor != null) { 945 monitor.showForegroundDialog(); 946 } 947 } 948 }); 949 950 Main.pref.addPreferenceChangeListener(this); 951 952 mvComponentAdapter = new ComponentAdapter() { 953 @Override 954 public void componentResized(ComponentEvent e) { 955 nameText.setCharCount(getNameLabelCharacterCount(Main.parent)); 956 revalidate(); 957 } 958 }; 959 mv.addComponentListener(mvComponentAdapter); 960 961 // The background thread 962 thread = new Thread(collector, "Map Status Collector"); 963 thread.setDaemon(true); 964 thread.start(); 965 } 966 967 @Override 968 public void systemOfMeasurementChanged(String oldSoM, String newSoM) { 969 setDist(distValue); 970 } 971 972 /** 973 * Updates the system of measurement and displays a notification. 974 * @param newsom The new system of measurement to set 975 * @since 6960 976 */ 977 public void updateSystemOfMeasurement(String newsom) { 978 SystemOfMeasurement.setSystemOfMeasurement(newsom); 979 if (Main.pref.getBoolean("statusbar.notify.change-system-of-measurement", true)) { 980 new Notification(tr("System of measurement changed to {0}", newsom)) 981 .setDuration(Notification.TIME_SHORT) 982 .show(); 983 } 984 } 985 986 public JPanel getAnglePanel() { 987 return angleText; 988 } 989 990 @Override 991 public String helpTopic() { 992 return ht("/StatusBar"); 993 } 994 995 @Override 996 public synchronized void addMouseListener(MouseListener ml) { 997 lonText.addMouseListener(ml); 998 latText.addMouseListener(ml); 999 } 1000 1001 public void setHelpText(String t) { 1002 setHelpText(null, t); 1003 } 1004 1005 public void setHelpText(Object id, final String text) { 1006 1007 StatusTextHistory entry = new StatusTextHistory(id, text); 1008 1009 statusText.remove(entry); 1010 statusText.add(entry); 1011 1012 GuiHelper.runInEDT(new Runnable() { 1013 @Override 1014 public void run() { 1015 helpText.setText(text); 1016 helpText.setToolTipText(text); 1017 } 1018 }); 1019 } 1020 1021 public void resetHelpText(Object id) { 1022 if (statusText.isEmpty()) 1023 return; 1024 1025 StatusTextHistory entry = new StatusTextHistory(id, null); 1026 if (statusText.get(statusText.size() - 1).equals(entry)) { 1027 if (statusText.size() == 1) { 1028 setHelpText(""); 1029 } else { 1030 StatusTextHistory history = statusText.get(statusText.size() - 2); 1031 setHelpText(history.id, history.text); 1032 } 1033 } 1034 statusText.remove(entry); 1035 } 1036 1037 public void setAngle(double a) { 1038 angleText.setText(a < 0 ? "--" : DECIMAL_FORMAT.format(a) + " \u00B0"); 1039 } 1040 1041 public void setHeading(double h) { 1042 headingText.setText(h < 0 ? "--" : DECIMAL_FORMAT.format(h) + " \u00B0"); 1043 } 1044 1045 /** 1046 * Sets the distance text to the given value 1047 * @param dist The distance value to display, in meters 1048 */ 1049 public void setDist(double dist) { 1050 distValue = dist; 1051 distText.setText(dist < 0 ? "--" : NavigatableComponent.getDistText(dist, DECIMAL_FORMAT, DISTANCE_THRESHOLD)); 1052 } 1053 1054 /** 1055 * Sets the distance text to the total sum of given ways length 1056 * @param ways The ways to consider for the total distance 1057 * @since 5991 1058 */ 1059 public void setDist(Collection<Way> ways) { 1060 double dist = -1; 1061 // Compute total length of selected way(s) until an arbitrary limit set to 250 ways 1062 // in order to prevent performance issue if a large number of ways are selected (old behaviour kept in that case, see #8403) 1063 int maxWays = Math.max(1, Main.pref.getInteger("selection.max-ways-for-statusline", 250)); 1064 if (!ways.isEmpty() && ways.size() <= maxWays) { 1065 dist = 0.0; 1066 for (Way w : ways) { 1067 dist += w.getLength(); 1068 } 1069 } 1070 setDist(dist); 1071 } 1072 1073 /** 1074 * Activates the angle panel. 1075 * @param activeFlag {@code true} to activate it, {@code false} to deactivate it 1076 */ 1077 public void activateAnglePanel(boolean activeFlag) { 1078 angleEnabled = activeFlag; 1079 refreshAnglePanel(); 1080 } 1081 1082 private void refreshAnglePanel() { 1083 angleText.setBackground(angleEnabled ? PROP_ACTIVE_BACKGROUND_COLOR.get() : PROP_BACKGROUND_COLOR.get()); 1084 angleText.setForeground(angleEnabled ? PROP_ACTIVE_FOREGROUND_COLOR.get() : PROP_FOREGROUND_COLOR.get()); 1085 } 1086 1087 @Override 1088 public void destroy() { 1089 SystemOfMeasurement.removeSoMChangeListener(this); 1090 Main.pref.removePreferenceChangeListener(this); 1091 mv.removeComponentListener(mvComponentAdapter); 1092 1093 // MapFrame gets destroyed when the last layer is removed, but the status line background 1094 // thread that collects the information doesn't get destroyed automatically. 1095 if (thread != null) { 1096 try { 1097 thread.interrupt(); 1098 } catch (RuntimeException e) { 1099 Main.error(e); 1100 } 1101 } 1102 } 1103 1104 @Override 1105 public void preferenceChanged(PreferenceChangeEvent e) { 1106 String key = e.getKey(); 1107 if (key.startsWith("color.")) { 1108 key = key.substring("color.".length()); 1109 if (PROP_BACKGROUND_COLOR.getKey().equals(key) || PROP_FOREGROUND_COLOR.getKey().equals(key)) { 1110 for (ImageLabel il : new ImageLabel[]{latText, lonText, headingText, distText, nameText}) { 1111 il.setBackground(PROP_BACKGROUND_COLOR.get()); 1112 il.setForeground(PROP_FOREGROUND_COLOR.get()); 1113 } 1114 refreshAnglePanel(); 1115 } else if (PROP_ACTIVE_BACKGROUND_COLOR.getKey().equals(key) || PROP_ACTIVE_FOREGROUND_COLOR.getKey().equals(key)) { 1116 refreshAnglePanel(); 1117 } 1118 } 1119 } 1120 1121 /** 1122 * Loads all colors from preferences. 1123 * @since 6789 1124 */ 1125 public static void getColors() { 1126 PROP_BACKGROUND_COLOR.get(); 1127 PROP_FOREGROUND_COLOR.get(); 1128 PROP_ACTIVE_BACKGROUND_COLOR.get(); 1129 PROP_ACTIVE_FOREGROUND_COLOR.get(); 1130 } 1131 1132 private static int getNameLabelCharacterCount(Component parent) { 1133 int w = parent != null ? parent.getWidth() : 800; 1134 return Math.min(80, 20 + Math.max(0, w-1280) * 60 / (1920-1280)); 1135 } 1136}