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

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

see #8465 - use diamond operator where applicable

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