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