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