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