source: josm/trunk/src/org/openstreetmap/josm/gui/MapStatus.java@ 9168

Last change on this file since 9168 was 9078, checked in by Don-vip, 8 years ago

sonar - Immutable Field

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