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 271 && oldMousePos.equals(ms.mousePos); 272 boolean isAtOldPosition = mouseNotMoved && popup != null; 273 boolean middleMouseDown = (ms.modifiers & MouseEvent.BUTTON2_DOWN_MASK) != 0; 274 try { 275 ds = mv.getCurrentDataSet(); 276 if (ds != null) { 277 // This is not perfect, if current dataset was changed during execution, the lock would be useless 278 if (isAtOldPosition && middleMouseDown) { 279 // Write lock is necessary when selecting in popupCycleSelection 280 // locks can not be upgraded -> if do read lock here and write lock later 281 // (in OsmPrimitive.updateFlags) then always occurs deadlock (#5814) 282 ds.beginUpdate(); 283 } else { 284 ds.getReadLock().lock(); 285 } 286 } 287 288 // Set the text label in the bottom status bar 289 // "if mouse moved only" was added to stop heap growing 290 if (!mouseNotMoved) { 291 statusBarElementUpdate(ms); 292 } 293 294 // Popup Information 295 // display them if the middle mouse button is pressed and keep them until the mouse is moved 296 if (middleMouseDown || isAtOldPosition) { 297 Collection<OsmPrimitive> osms = mv.getAllNearest(ms.mousePos, new Predicate<OsmPrimitive>() { 298 @Override 299 public boolean evaluate(OsmPrimitive o) { 300 return isUsablePredicate.evaluate(o) && isSelectablePredicate.evaluate(o); 301 } 302 }); 303 304 final JPanel c = new JPanel(new GridBagLayout()); 305 final JLabel lbl = new JLabel( 306 "<html>"+tr("Middle click again to cycle through.<br>"+ 307 "Hold CTRL to select directly from this list with the mouse.<hr>")+"</html>", 308 null, 309 JLabel.HORIZONTAL 310 ); 311 lbl.setHorizontalAlignment(JLabel.LEFT); 312 c.add(lbl, GBC.eol().insets(2, 0, 2, 0)); 313 314 // Only cycle if the mouse has not been moved and the middle mouse button has been pressed at least 315 // twice (the reason for this is the popup != null check for isAtOldPosition, see above. 316 // This is a nice side effect though, because it does not change selection of the first middle click) 317 if (isAtOldPosition && middleMouseDown) { 318 // Hand down mouse modifiers so the SHIFT mod can be handled correctly (see function) 319 popupCycleSelection(osms, ms.modifiers); 320 } 321 322 // These labels may need to be updated from the outside so collect them 323 List<JLabel> lbls = new ArrayList<>(osms.size()); 324 for (final OsmPrimitive osm : osms) { 325 JLabel l = popupBuildPrimitiveLabels(osm); 326 lbls.add(l); 327 c.add(l, GBC.eol().fill(GBC.HORIZONTAL).insets(2, 0, 2, 2)); 328 } 329 330 popupShowPopup(popupCreatePopup(c, ms), lbls); 331 } else { 332 popupHidePopup(); 333 } 334 335 oldMousePos = ms.mousePos; 336 } catch (ConcurrentModificationException x) { 337 Main.warn(x); 338 } finally { 339 if (ds != null) { 340 if (isAtOldPosition && middleMouseDown) { 341 ds.endUpdate(); 342 } else { 343 ds.getReadLock().unlock(); 344 } 345 } 346 } 347 } 348 } 349 350 /** 351 * the mouse position of the previous iteration. This is used to show 352 * the popup until the cursor is moved. 353 */ 354 private Point oldMousePos; 355 /** 356 * Contains the labels that are currently shown in the information 357 * popup 358 */ 359 private List<JLabel> popupLabels; 360 /** 361 * The popup displayed to show additional information 362 */ 363 private Popup popup; 364 365 private final MapFrame parent; 366 367 private final BlockingQueue<MouseState> incomingMouseState = new LinkedBlockingQueue<>(); 368 369 private Point lastMousePos; 370 371 Collector(MapFrame parent) { 372 this.parent = parent; 373 } 374 375 /** 376 * Execution function for the Collector. 377 */ 378 @Override 379 public void run() { 380 registerListeners(); 381 try { 382 for (;;) { 383 try { 384 final MouseState ms = incomingMouseState.take(); 385 if (parent != Main.map) 386 return; // exit, if new parent. 387 388 // Do nothing, if required data is missing 389 if (ms.mousePos == null || mv.center == null) { 390 continue; 391 } 392 393 EventQueue.invokeAndWait(new CollectorWorker(ms)); 394 } catch (InterruptedException e) { 395 // Occurs frequently during JOSM shutdown, log set to trace only 396 Main.trace("InterruptedException in "+MapStatus.class.getSimpleName()); 397 } catch (InvocationTargetException e) { 398 Main.warn(e); 399 } 400 } 401 } finally { 402 unregisterListeners(); 403 } 404 } 405 406 /** 407 * Creates a popup for the given content next to the cursor. Tries to 408 * keep the popup on screen and shows a vertical scrollbar, if the 409 * screen is too small. 410 * @param content popup content 411 * @param ms mouse state 412 * @return popup 413 */ 414 private Popup popupCreatePopup(Component content, MouseState ms) { 415 Point p = mv.getLocationOnScreen(); 416 Dimension scrn = GuiHelper.getScreenSize(); 417 418 // Create a JScrollPane around the content, in case there's not enough space 419 JScrollPane sp = GuiHelper.embedInVerticalScrollPane(content); 420 sp.setBorder(BorderFactory.createRaisedBevelBorder()); 421 // Implement max-size content-independent 422 Dimension prefsize = sp.getPreferredSize(); 423 int w = Math.min(prefsize.width, Math.min(800, (scrn.width/2) - 16)); 424 int h = Math.min(prefsize.height, scrn.height - 10); 425 sp.setPreferredSize(new Dimension(w, h)); 426 427 int xPos = p.x + ms.mousePos.x + 16; 428 // Display the popup to the left of the cursor if it would be cut 429 // off on its right, but only if more space is available 430 if (xPos + w > scrn.width && xPos > scrn.width/2) { 431 xPos = p.x + ms.mousePos.x - 4 - w; 432 } 433 int yPos = p.y + ms.mousePos.y + 16; 434 // Move the popup up if it would be cut off at its bottom but do not 435 // move it off screen on the top 436 if (yPos + h > scrn.height - 5) { 437 yPos = Math.max(5, scrn.height - h - 5); 438 } 439 440 PopupFactory pf = PopupFactory.getSharedInstance(); 441 return pf.getPopup(mv, sp, xPos, yPos); 442 } 443 444 /** 445 * Calls this to update the element that is shown in the statusbar 446 * @param ms mouse state 447 */ 448 private void statusBarElementUpdate(MouseState ms) { 449 final OsmPrimitive osmNearest = mv.getNearestNodeOrWay(ms.mousePos, isUsablePredicate, false); 450 if (osmNearest != null) { 451 nameText.setText(osmNearest.getDisplayName(DefaultNameFormatter.getInstance())); 452 } else { 453 nameText.setText(tr("(no object)")); 454 } 455 } 456 457 /** 458 * Call this with a set of primitives to cycle through them. Method 459 * will automatically select the next item and update the map 460 * @param osms primitives to cycle through 461 * @param mods modifiers (i.e. control keys) 462 */ 463 private void popupCycleSelection(Collection<OsmPrimitive> osms, int mods) { 464 DataSet ds = Main.main.getCurrentDataSet(); 465 // Find some items that are required for cycling through 466 OsmPrimitive firstItem = null; 467 OsmPrimitive firstSelected = null; 468 OsmPrimitive nextSelected = null; 469 for (final OsmPrimitive osm : osms) { 470 if (firstItem == null) { 471 firstItem = osm; 472 } 473 if (firstSelected != null && nextSelected == null) { 474 nextSelected = osm; 475 } 476 if (firstSelected == null && ds.isSelected(osm)) { 477 firstSelected = osm; 478 } 479 } 480 481 // Clear previous selection if SHIFT (add to selection) is not 482 // pressed. Cannot use "setSelected()" because it will cause a 483 // fireSelectionChanged event which is unnecessary at this point. 484 if ((mods & MouseEvent.SHIFT_DOWN_MASK) == 0) { 485 ds.clearSelection(); 486 } 487 488 // This will cycle through the available items. 489 if (firstSelected != null) { 490 ds.clearSelection(firstSelected); 491 if (nextSelected != null) { 492 ds.addSelected(nextSelected); 493 } 494 } else if (firstItem != null) { 495 ds.addSelected(firstItem); 496 } 497 } 498 499 /** 500 * Tries to hide the given popup 501 */ 502 private void popupHidePopup() { 503 popupLabels = null; 504 if (popup == null) 505 return; 506 final Popup staticPopup = popup; 507 popup = null; 508 EventQueue.invokeLater(new Runnable() { 509 @Override 510 public void run() { 511 staticPopup.hide(); 512 } 513 }); 514 } 515 516 /** 517 * Tries to show the given popup, can be hidden using {@link #popupHidePopup} 518 * If an old popup exists, it will be automatically hidden 519 * @param newPopup popup to show 520 * @param lbls lables to show (see {@link #popupLabels}) 521 */ 522 private void popupShowPopup(Popup newPopup, List<JLabel> lbls) { 523 final Popup staticPopup = newPopup; 524 if (this.popup != null) { 525 // If an old popup exists, remove it when the new popup has been drawn to keep flickering to a minimum 526 final Popup staticOldPopup = this.popup; 527 EventQueue.invokeLater(new Runnable() { 528 @Override 529 public void run() { 530 staticPopup.show(); 531 staticOldPopup.hide(); 532 } 533 }); 534 } else { 535 // There is no old popup 536 EventQueue.invokeLater(new Runnable() { 537 @Override 538 public void run() { 539 staticPopup.show(); 540 } 541 }); 542 } 543 this.popupLabels = lbls; 544 this.popup = newPopup; 545 } 546 547 /** 548 * This method should be called if the selection may have changed from 549 * outside of this class. This is the case when CTRL is pressed and the 550 * user clicks on the map instead of the popup. 551 */ 552 private void popupUpdateLabels() { 553 if (this.popup == null || this.popupLabels == null) 554 return; 555 for (JLabel l : this.popupLabels) { 556 l.validate(); 557 } 558 } 559 560 /** 561 * Sets the colors for the given label depending on the selected status of 562 * the given OsmPrimitive 563 * 564 * @param lbl The label to color 565 * @param osm The primitive to derive the colors from 566 */ 567 private void popupSetLabelColors(JLabel lbl, OsmPrimitive osm) { 568 DataSet ds = Main.main.getCurrentDataSet(); 569 if (ds.isSelected(osm)) { 570 lbl.setBackground(SystemColor.textHighlight); 571 lbl.setForeground(SystemColor.textHighlightText); 572 } else { 573 lbl.setBackground(SystemColor.control); 574 lbl.setForeground(SystemColor.controlText); 575 } 576 } 577 578 /** 579 * Builds the labels with all necessary listeners for the info popup for the 580 * given OsmPrimitive 581 * @param osm The primitive to create the label for 582 * @return labels for info popup 583 */ 584 private JLabel popupBuildPrimitiveLabels(final OsmPrimitive osm) { 585 final StringBuilder text = new StringBuilder(32); 586 String name = osm.getDisplayName(DefaultNameFormatter.getInstance()); 587 if (osm.isNewOrUndeleted() || osm.isModified()) { 588 name = "<i><b>"+ name + "*</b></i>"; 589 } 590 text.append(name); 591 592 boolean idShown = Main.pref.getBoolean("osm-primitives.showid"); 593 // fix #7557 - do not show ID twice 594 595 if (!osm.isNew() && !idShown) { 596 text.append(" [id=").append(osm.getId()).append(']'); 597 } 598 599 if (osm.getUser() != null) { 600 text.append(" [").append(tr("User:")).append(' ').append(osm.getUser().getName()).append(']'); 601 } 602 603 for (String key : osm.keySet()) { 604 text.append("<br>").append(key).append('=').append(osm.get(key)); 605 } 606 607 final JLabel l = new JLabel( 608 "<html>" + text.toString() + "</html>", 609 ImageProvider.get(osm.getDisplayType()), 610 JLabel.HORIZONTAL 611 ) { 612 // This is necessary so the label updates its colors when the 613 // selection is changed from the outside 614 @Override 615 public void validate() { 616 super.validate(); 617 popupSetLabelColors(this, osm); 618 } 619 }; 620 l.setOpaque(true); 621 popupSetLabelColors(l, osm); 622 l.setFont(l.getFont().deriveFont(Font.PLAIN)); 623 l.setVerticalTextPosition(JLabel.TOP); 624 l.setHorizontalAlignment(JLabel.LEFT); 625 l.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); 626 l.addMouseListener(new MouseAdapter() { 627 @Override 628 public void mouseEntered(MouseEvent e) { 629 l.setBackground(SystemColor.info); 630 l.setForeground(SystemColor.infoText); 631 } 632 633 @Override 634 public void mouseExited(MouseEvent e) { 635 popupSetLabelColors(l, osm); 636 } 637 638 @Override 639 public void mouseClicked(MouseEvent e) { 640 DataSet ds = Main.main.getCurrentDataSet(); 641 // Let the user toggle the selection 642 ds.toggleSelected(osm); 643 l.validate(); 644 } 645 }); 646 // Sometimes the mouseEntered event is not catched, thus the label 647 // will not be highlighted, making it confusing. The MotionListener can correct this defect. 648 l.addMouseMotionListener(new MouseMotionListener() { 649 @Override 650 public void mouseMoved(MouseEvent e) { 651 l.setBackground(SystemColor.info); 652 l.setForeground(SystemColor.infoText); 653 } 654 655 @Override 656 public void mouseDragged(MouseEvent e) { 657 l.setBackground(SystemColor.info); 658 l.setForeground(SystemColor.infoText); 659 } 660 }); 661 return l; 662 } 663 664 /** 665 * Called whenever the mouse position or modifiers changed. 666 * @param mousePos The new mouse position. <code>null</code> if it did not change. 667 * @param modifiers The new modifiers. 668 */ 669 public synchronized void updateMousePosition(Point mousePos, int modifiers) { 670 if (mousePos != null) { 671 lastMousePos = mousePos; 672 } 673 MouseState ms = new MouseState(lastMousePos, modifiers); 674 // remove mouse states that are in the queue. Our mouse state is newer. 675 incomingMouseState.clear(); 676 if (!incomingMouseState.offer(ms)) { 677 Main.warn("Unable to handle new MouseState: " + ms); 678 } 679 } 680 } 681 682 /** 683 * Everything, the collector is interested of. Access must be synchronized. 684 * @author imi 685 */ 686 private static class MouseState { 687 private final Point mousePos; 688 private final int modifiers; 689 690 MouseState(Point mousePos, int modifiers) { 691 this.mousePos = mousePos; 692 this.modifiers = modifiers; 693 } 694 } 695 696 private final transient AWTEventListener awtListener = new AWTEventListener() { 697 @Override 698 public void eventDispatched(AWTEvent event) { 699 if (event instanceof InputEvent && 700 ((InputEvent) event).getComponent() == mv) { 701 synchronized (collector) { 702 int modifiers = ((InputEvent) event).getModifiersEx(); 703 Point mousePos = null; 704 if (event instanceof MouseEvent) { 705 mousePos = ((MouseEvent) event).getPoint(); 706 } 707 collector.updateMousePosition(mousePos, modifiers); 708 } 709 } 710 } 711 }; 712 713 private final transient MouseMotionListener mouseMotionListener = new MouseMotionListener() { 714 @Override 715 public void mouseMoved(MouseEvent e) { 716 synchronized (collector) { 717 collector.updateMousePosition(e.getPoint(), e.getModifiersEx()); 718 } 719 } 720 721 @Override 722 public void mouseDragged(MouseEvent e) { 723 mouseMoved(e); 724 } 725 }; 726 727 private final transient KeyAdapter keyAdapter = new KeyAdapter() { 728 @Override public void keyPressed(KeyEvent e) { 729 synchronized (collector) { 730 collector.updateMousePosition(null, e.getModifiersEx()); 731 } 732 } 733 734 @Override public void keyReleased(KeyEvent e) { 735 keyPressed(e); 736 } 737 }; 738 739 private void registerListeners() { 740 // Listen to keyboard/mouse events for pressing/releasing alt key and 741 // inform the collector. 742 try { 743 Toolkit.getDefaultToolkit().addAWTEventListener(awtListener, 744 AWTEvent.KEY_EVENT_MASK | AWTEvent.MOUSE_EVENT_MASK | AWTEvent.MOUSE_MOTION_EVENT_MASK); 745 } catch (SecurityException ex) { 746 mv.addMouseMotionListener(mouseMotionListener); 747 mv.addKeyListener(keyAdapter); 748 } 749 } 750 751 private void unregisterListeners() { 752 try { 753 Toolkit.getDefaultToolkit().removeAWTEventListener(awtListener); 754 } catch (SecurityException e) { 755 // Don't care, awtListener probably wasn't registered anyway 756 if (Main.isTraceEnabled()) { 757 Main.trace(e.getMessage()); 758 } 759 } 760 mv.removeMouseMotionListener(mouseMotionListener); 761 mv.removeKeyListener(keyAdapter); 762 } 763 764 private class MapStatusPopupMenu extends JPopupMenu { 765 766 private final JMenuItem jumpButton = add(Main.main.menu.jumpToAct); 767 768 /** Icons for selecting {@link SystemOfMeasurement} */ 769 private final Collection<JCheckBoxMenuItem> somItems = new ArrayList<>(); 770 /** Icons for selecting {@link CoordinateFormat} */ 771 private final Collection<JCheckBoxMenuItem> coordinateFormatItems = new ArrayList<>(); 772 773 private final JSeparator separator = new JSeparator(); 774 775 private final JMenuItem doNotHide = new JCheckBoxMenuItem(new AbstractAction(tr("Do not hide status bar")) { 776 @Override 777 public void actionPerformed(ActionEvent e) { 778 boolean sel = ((JCheckBoxMenuItem) e.getSource()).getState(); 779 Main.pref.put("statusbar.always-visible", sel); 780 } 781 }); 782 783 MapStatusPopupMenu() { 784 for (final String key : new TreeSet<>(SystemOfMeasurement.ALL_SYSTEMS.keySet())) { 785 JCheckBoxMenuItem item = new JCheckBoxMenuItem(new AbstractAction(key) { 786 @Override 787 public void actionPerformed(ActionEvent e) { 788 updateSystemOfMeasurement(key); 789 } 790 }); 791 somItems.add(item); 792 add(item); 793 } 794 for (final CoordinateFormat format : CoordinateFormat.values()) { 795 JCheckBoxMenuItem item = new JCheckBoxMenuItem(new AbstractAction(format.getDisplayName()) { 796 @Override 797 public void actionPerformed(ActionEvent e) { 798 CoordinateFormat.setCoordinateFormat(format); 799 } 800 }); 801 coordinateFormatItems.add(item); 802 add(item); 803 } 804 805 add(separator); 806 add(doNotHide); 807 808 addPopupMenuListener(new PopupMenuListener() { 809 @Override 810 public void popupMenuWillBecomeVisible(PopupMenuEvent e) { 811 Component invoker = ((JPopupMenu) e.getSource()).getInvoker(); 812 jumpButton.setVisible(latText.equals(invoker) || lonText.equals(invoker)); 813 String currentSOM = ProjectionPreference.PROP_SYSTEM_OF_MEASUREMENT.get(); 814 for (JMenuItem item : somItems) { 815 item.setSelected(item.getText().equals(currentSOM)); 816 item.setVisible(distText.equals(invoker)); 817 } 818 final String currentCorrdinateFormat = CoordinateFormat.getDefaultFormat().getDisplayName(); 819 for (JMenuItem item : coordinateFormatItems) { 820 item.setSelected(currentCorrdinateFormat.equals(item.getText())); 821 item.setVisible(latText.equals(invoker) || lonText.equals(invoker)); 822 } 823 separator.setVisible(distText.equals(invoker) || latText.equals(invoker) || lonText.equals(invoker)); 824 doNotHide.setSelected(Main.pref.getBoolean("statusbar.always-visible", true)); 825 } 826 827 @Override 828 public void popupMenuWillBecomeInvisible(PopupMenuEvent e) { 829 // Do nothing 830 } 831 832 @Override 833 public void popupMenuCanceled(PopupMenuEvent e) { 834 // Do nothing 835 } 836 }); 837 } 838 } 839 840 /** 841 * Construct a new MapStatus and attach it to the map view. 842 * @param mapFrame The MapFrame the status line is part of. 843 */ 844 public MapStatus(final MapFrame mapFrame) { 845 this.mv = mapFrame.mapView; 846 this.collector = new Collector(mapFrame); 847 848 // Context menu of status bar 849 setComponentPopupMenu(new MapStatusPopupMenu()); 850 851 // also show Jump To dialog on mouse click (except context menu) 852 MouseListener jumpToOnLeftClick = new MouseAdapter() { 853 @Override 854 public void mouseClicked(MouseEvent e) { 855 if (e.getButton() != MouseEvent.BUTTON3) { 856 Main.main.menu.jumpToAct.showJumpToDialog(); 857 } 858 } 859 }; 860 861 // Listen for mouse movements and set the position text field 862 mv.addMouseMotionListener(new MouseMotionListener() { 863 @Override 864 public void mouseDragged(MouseEvent e) { 865 mouseMoved(e); 866 } 867 868 @Override 869 public void mouseMoved(MouseEvent e) { 870 if (mv.center == null) 871 return; 872 // Do not update the view if ctrl is pressed. 873 if ((e.getModifiersEx() & MouseEvent.CTRL_DOWN_MASK) == 0) { 874 CoordinateFormat mCord = CoordinateFormat.getDefaultFormat(); 875 LatLon p = mv.getLatLon(e.getX(), e.getY()); 876 latText.setText(p.latToString(mCord)); 877 lonText.setText(p.lonToString(mCord)); 878 if (Objects.equals(previousCoordinateFormat, mCord)) { 879 // do nothing 880 } else if (CoordinateFormat.EAST_NORTH.equals(mCord)) { 881 latText.setIcon("northing"); 882 lonText.setIcon("easting"); 883 latText.setToolTipText(tr("The northing at the mouse pointer.")); 884 lonText.setToolTipText(tr("The easting at the mouse pointer.")); 885 previousCoordinateFormat = mCord; 886 } else { 887 latText.setIcon("lat"); 888 lonText.setIcon("lon"); 889 latText.setToolTipText(tr("The geographic latitude at the mouse pointer.")); 890 lonText.setToolTipText(tr("The geographic longitude at the mouse pointer.")); 891 previousCoordinateFormat = mCord; 892 } 893 } 894 } 895 }); 896 897 setLayout(new GridBagLayout()); 898 setBorder(BorderFactory.createEmptyBorder(1, 2, 1, 2)); 899 900 latText.setInheritsPopupMenu(true); 901 lonText.setInheritsPopupMenu(true); 902 headingText.setInheritsPopupMenu(true); 903 distText.setInheritsPopupMenu(true); 904 nameText.setInheritsPopupMenu(true); 905 906 add(latText, GBC.std()); 907 add(lonText, GBC.std().insets(3, 0, 0, 0)); 908 add(headingText, GBC.std().insets(3, 0, 0, 0)); 909 add(angleText, GBC.std().insets(3, 0, 0, 0)); 910 add(distText, GBC.std().insets(3, 0, 0, 0)); 911 912 if (Main.pref.getBoolean("statusbar.change-system-of-measurement-on-click", true)) { 913 distText.addMouseListener(new MouseAdapter() { 914 private final List<String> soms = new ArrayList<>(new TreeSet<>(SystemOfMeasurement.ALL_SYSTEMS.keySet())); 915 916 @Override 917 public void mouseClicked(MouseEvent e) { 918 if (!e.isPopupTrigger() && e.getButton() == MouseEvent.BUTTON1) { 919 String som = ProjectionPreference.PROP_SYSTEM_OF_MEASUREMENT.get(); 920 String newsom = soms.get((soms.indexOf(som)+1) % soms.size()); 921 updateSystemOfMeasurement(newsom); 922 } 923 } 924 }); 925 } 926 927 SystemOfMeasurement.addSoMChangeListener(this); 928 929 latText.addMouseListener(jumpToOnLeftClick); 930 lonText.addMouseListener(jumpToOnLeftClick); 931 932 helpText.setEditable(false); 933 add(nameText, GBC.std().insets(3, 0, 0, 0)); 934 add(helpText, GBC.std().insets(3, 0, 0, 0).fill(GBC.HORIZONTAL)); 935 936 progressBar.setMaximum(PleaseWaitProgressMonitor.PROGRESS_BAR_MAX); 937 progressBar.setVisible(false); 938 GBC gbc = GBC.eol(); 939 gbc.ipadx = 100; 940 add(progressBar, gbc); 941 progressBar.addMouseListener(new MouseAdapter() { 942 @Override 943 public void mouseClicked(MouseEvent e) { 944 PleaseWaitProgressMonitor monitor = Main.currentProgressMonitor; 945 if (monitor != null) { 946 monitor.showForegroundDialog(); 947 } 948 } 949 }); 950 951 Main.pref.addPreferenceChangeListener(this); 952 953 mvComponentAdapter = new ComponentAdapter() { 954 @Override 955 public void componentResized(ComponentEvent e) { 956 nameText.setCharCount(getNameLabelCharacterCount(Main.parent)); 957 revalidate(); 958 } 959 }; 960 mv.addComponentListener(mvComponentAdapter); 961 962 // The background thread 963 thread = new Thread(collector, "Map Status Collector"); 964 thread.setDaemon(true); 965 thread.start(); 966 } 967 968 @Override 969 public void systemOfMeasurementChanged(String oldSoM, String newSoM) { 970 setDist(distValue); 971 } 972 973 /** 974 * Updates the system of measurement and displays a notification. 975 * @param newsom The new system of measurement to set 976 * @since 6960 977 */ 978 public void updateSystemOfMeasurement(String newsom) { 979 SystemOfMeasurement.setSystemOfMeasurement(newsom); 980 if (Main.pref.getBoolean("statusbar.notify.change-system-of-measurement", true)) { 981 new Notification(tr("System of measurement changed to {0}", newsom)) 982 .setDuration(Notification.TIME_SHORT) 983 .show(); 984 } 985 } 986 987 public JPanel getAnglePanel() { 988 return angleText; 989 } 990 991 @Override 992 public String helpTopic() { 993 return ht("/StatusBar"); 994 } 995 996 @Override 997 public synchronized void addMouseListener(MouseListener ml) { 998 lonText.addMouseListener(ml); 999 latText.addMouseListener(ml); 1000 } 1001 1002 public void setHelpText(String t) { 1003 setHelpText(null, t); 1004 } 1005 1006 public void setHelpText(Object id, final String text) { 1007 1008 StatusTextHistory entry = new StatusTextHistory(id, text); 1009 1010 statusText.remove(entry); 1011 statusText.add(entry); 1012 1013 GuiHelper.runInEDT(new Runnable() { 1014 @Override 1015 public void run() { 1016 helpText.setText(text); 1017 helpText.setToolTipText(text); 1018 } 1019 }); 1020 } 1021 1022 public void resetHelpText(Object id) { 1023 if (statusText.isEmpty()) 1024 return; 1025 1026 StatusTextHistory entry = new StatusTextHistory(id, null); 1027 if (statusText.get(statusText.size() - 1).equals(entry)) { 1028 if (statusText.size() == 1) { 1029 setHelpText(""); 1030 } else { 1031 StatusTextHistory history = statusText.get(statusText.size() - 2); 1032 setHelpText(history.id, history.text); 1033 } 1034 } 1035 statusText.remove(entry); 1036 } 1037 1038 public void setAngle(double a) { 1039 angleText.setText(a < 0 ? "--" : DECIMAL_FORMAT.format(a) + " \u00B0"); 1040 } 1041 1042 public void setHeading(double h) { 1043 headingText.setText(h < 0 ? "--" : DECIMAL_FORMAT.format(h) + " \u00B0"); 1044 } 1045 1046 /** 1047 * Sets the distance text to the given value 1048 * @param dist The distance value to display, in meters 1049 */ 1050 public void setDist(double dist) { 1051 distValue = dist; 1052 distText.setText(dist < 0 ? "--" : NavigatableComponent.getDistText(dist, DECIMAL_FORMAT, DISTANCE_THRESHOLD)); 1053 } 1054 1055 /** 1056 * Sets the distance text to the total sum of given ways length 1057 * @param ways The ways to consider for the total distance 1058 * @since 5991 1059 */ 1060 public void setDist(Collection<Way> ways) { 1061 double dist = -1; 1062 // Compute total length of selected way(s) until an arbitrary limit set to 250 ways 1063 // in order to prevent performance issue if a large number of ways are selected (old behaviour kept in that case, see #8403) 1064 int maxWays = Math.max(1, Main.pref.getInteger("selection.max-ways-for-statusline", 250)); 1065 if (!ways.isEmpty() && ways.size() <= maxWays) { 1066 dist = 0.0; 1067 for (Way w : ways) { 1068 dist += w.getLength(); 1069 } 1070 } 1071 setDist(dist); 1072 } 1073 1074 /** 1075 * Activates the angle panel. 1076 * @param activeFlag {@code true} to activate it, {@code false} to deactivate it 1077 */ 1078 public void activateAnglePanel(boolean activeFlag) { 1079 angleEnabled = activeFlag; 1080 refreshAnglePanel(); 1081 } 1082 1083 private void refreshAnglePanel() { 1084 angleText.setBackground(angleEnabled ? PROP_ACTIVE_BACKGROUND_COLOR.get() : PROP_BACKGROUND_COLOR.get()); 1085 angleText.setForeground(angleEnabled ? PROP_ACTIVE_FOREGROUND_COLOR.get() : PROP_FOREGROUND_COLOR.get()); 1086 } 1087 1088 @Override 1089 public void destroy() { 1090 SystemOfMeasurement.removeSoMChangeListener(this); 1091 Main.pref.removePreferenceChangeListener(this); 1092 mv.removeComponentListener(mvComponentAdapter); 1093 1094 // MapFrame gets destroyed when the last layer is removed, but the status line background 1095 // thread that collects the information doesn't get destroyed automatically. 1096 if (thread != null) { 1097 try { 1098 thread.interrupt(); 1099 } catch (RuntimeException e) { 1100 Main.error(e); 1101 } 1102 } 1103 } 1104 1105 @Override 1106 public void preferenceChanged(PreferenceChangeEvent e) { 1107 String key = e.getKey(); 1108 if (key.startsWith("color.")) { 1109 key = key.substring("color.".length()); 1110 if (PROP_BACKGROUND_COLOR.getKey().equals(key) || PROP_FOREGROUND_COLOR.getKey().equals(key)) { 1111 for (ImageLabel il : new ImageLabel[]{latText, lonText, headingText, distText, nameText}) { 1112 il.setBackground(PROP_BACKGROUND_COLOR.get()); 1113 il.setForeground(PROP_FOREGROUND_COLOR.get()); 1114 } 1115 refreshAnglePanel(); 1116 } else if (PROP_ACTIVE_BACKGROUND_COLOR.getKey().equals(key) || PROP_ACTIVE_FOREGROUND_COLOR.getKey().equals(key)) { 1117 refreshAnglePanel(); 1118 } 1119 } 1120 } 1121 1122 /** 1123 * Loads all colors from preferences. 1124 * @since 6789 1125 */ 1126 public static void getColors() { 1127 PROP_BACKGROUND_COLOR.get(); 1128 PROP_FOREGROUND_COLOR.get(); 1129 PROP_ACTIVE_BACKGROUND_COLOR.get(); 1130 PROP_ACTIVE_FOREGROUND_COLOR.get(); 1131 } 1132 1133 private static int getNameLabelCharacterCount(Component parent) { 1134 int w = parent != null ? parent.getWidth() : 800; 1135 return Math.min(80, 20 + Math.max(0, w-1280) * 60 / (1920-1280)); 1136 } 1137}