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

Last change on this file since 6798 was 6798, checked in by Don-vip, 10 years ago

fix some Sonar issues

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