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