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