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