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

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

fix #8859, fix #9893 - Change of System of Measurement (SoM) in map status:

  • keep current behaviour (left click) but can be deactivated with statusbar.change-system-of-measurement-on-click
  • add all SoMs in contextual menu (right-click) to have a direct access without cycling entirely
  • notify user when SoM changes (can be deactivated statusbar.notify.change-system-of-measurement)
  • 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.Preferences.PreferenceChangeEvent;
53import org.openstreetmap.josm.data.Preferences.PreferenceChangedListener;
54import org.openstreetmap.josm.data.coor.CoordinateFormat;
55import org.openstreetmap.josm.data.coor.LatLon;
56import org.openstreetmap.josm.data.osm.DataSet;
57import org.openstreetmap.josm.data.osm.OsmPrimitive;
58import org.openstreetmap.josm.data.osm.Way;
59import org.openstreetmap.josm.data.preferences.ColorProperty;
60import org.openstreetmap.josm.gui.NavigatableComponent.SoMChangeListener;
61import org.openstreetmap.josm.gui.help.Helpful;
62import org.openstreetmap.josm.gui.preferences.projection.ProjectionPreference;
63import org.openstreetmap.josm.gui.progress.PleaseWaitProgressMonitor;
64import org.openstreetmap.josm.gui.progress.PleaseWaitProgressMonitor.ProgressMonitorDialog;
65import org.openstreetmap.josm.gui.util.GuiHelper;
66import org.openstreetmap.josm.gui.widgets.ImageLabel;
67import org.openstreetmap.josm.gui.widgets.JosmTextField;
68import org.openstreetmap.josm.tools.Destroyable;
69import org.openstreetmap.josm.tools.GBC;
70import org.openstreetmap.josm.tools.ImageProvider;
71
72/**
73 * A component that manages some status information display about the map.
74 * It keeps a status line below the map up to date and displays some tooltip
75 * information if the user hold the mouse long enough at some point.
76 *
77 * All this is done in background to not disturb other processes.
78 *
79 * The background thread does not alter any data of the map (read only thread).
80 * Also it is rather fail safe. In case of some error in the data, it just does
81 * nothing instead of whining and complaining.
82 *
83 * @author imi
84 */
85public class MapStatus extends JPanel implements Helpful, Destroyable, PreferenceChangedListener {
86
87 /**
88 * Property for map status background color.
89 * @since 6789
90 */
91 public static final ColorProperty PROP_BACKGROUND_COLOR = new ColorProperty(
92 marktr("Status bar background"), Color.decode("#b8cfe5"));
93
94 /**
95 * Property for map status background color (active state).
96 * @since 6789
97 */
98 public static final ColorProperty PROP_ACTIVE_BACKGROUND_COLOR = new ColorProperty(
99 marktr("Status bar background: active"), Color.decode("#aaff5e"));
100
101 /**
102 * Property for map status foreground color.
103 * @since 6789
104 */
105 public static final ColorProperty PROP_FOREGROUND_COLOR = new ColorProperty(
106 marktr("Status bar foreground"), Color.black);
107
108 /**
109 * Property for map status foreground color (active state).
110 * @since 6789
111 */
112 public static final ColorProperty PROP_ACTIVE_FOREGROUND_COLOR = new ColorProperty(
113 marktr("Status bar foreground: active"), Color.black);
114
115 /**
116 * The MapView this status belongs to.
117 */
118 final MapView mv;
119 final Collector collector;
120
121 public class BackgroundProgressMonitor implements ProgressMonitorDialog {
122
123 private String title;
124 private String customText;
125
126 private void updateText() {
127 if (customText != null && !customText.isEmpty()) {
128 progressBar.setToolTipText(tr("{0} ({1})", title, customText));
129 } else {
130 progressBar.setToolTipText(title);
131 }
132 }
133
134 @Override
135 public void setVisible(boolean visible) {
136 progressBar.setVisible(visible);
137 }
138
139 @Override
140 public void updateProgress(int progress) {
141 progressBar.setValue(progress);
142 progressBar.repaint();
143 MapStatus.this.doLayout();
144 }
145
146 @Override
147 public void setCustomText(String text) {
148 this.customText = text;
149 updateText();
150 }
151
152 @Override
153 public void setCurrentAction(String text) {
154 this.title = text;
155 updateText();
156 }
157
158 @Override
159 public void setIndeterminate(boolean newValue) {
160 UIManager.put("ProgressBar.cycleTime", UIManager.getInt("ProgressBar.repaintInterval") * 100);
161 progressBar.setIndeterminate(newValue);
162 }
163
164 @Override
165 public void appendLogMessage(String message) {
166 if (message != null && !message.isEmpty()) {
167 Main.info("appendLogMessage not implemented for background tasks. Message was: " + message);
168 }
169 }
170
171 }
172
173 final ImageLabel latText = new ImageLabel("lat", tr("The geographic latitude at the mouse pointer."), 11, PROP_BACKGROUND_COLOR.get());
174 final ImageLabel lonText = new ImageLabel("lon", tr("The geographic longitude at the mouse pointer."), 11, PROP_BACKGROUND_COLOR.get());
175 final ImageLabel headingText = new ImageLabel("heading", tr("The (compass) heading of the line segment being drawn."), 6, PROP_BACKGROUND_COLOR.get());
176 final ImageLabel angleText = new ImageLabel("angle", tr("The angle between the previous and the current way segment."), 6, PROP_BACKGROUND_COLOR.get());
177 final ImageLabel distText = new ImageLabel("dist", tr("The length of the new way segment being drawn."), 10, PROP_BACKGROUND_COLOR.get());
178 final ImageLabel nameText = new ImageLabel("name", tr("The name of the object at the mouse pointer."), 20, PROP_BACKGROUND_COLOR.get());
179 final JosmTextField helpText = new JosmTextField();
180 final JProgressBar progressBar = new JProgressBar();
181 public final BackgroundProgressMonitor progressMonitor = new BackgroundProgressMonitor();
182
183 private final SoMChangeListener somListener;
184
185 // Distance value displayed in distText, stored if refresh needed after a change of system of measurement
186 private double distValue;
187
188 // Determines if angle panel is enabled or not
189 private boolean angleEnabled = false;
190
191 /**
192 * This is the thread that runs in the background and collects the information displayed.
193 * It gets destroyed by destroy() when the MapFrame itself is destroyed.
194 */
195 private Thread thread;
196
197 private final List<StatusTextHistory> statusText = new ArrayList<StatusTextHistory>();
198
199 private static class StatusTextHistory {
200 final Object id;
201 final String text;
202
203 public StatusTextHistory(Object id, String text) {
204 this.id = id;
205 this.text = text;
206 }
207
208 @Override
209 public boolean equals(Object obj) {
210 return obj instanceof StatusTextHistory && ((StatusTextHistory)obj).id == id;
211 }
212
213 @Override
214 public int hashCode() {
215 return System.identityHashCode(id);
216 }
217 }
218
219 /**
220 * The collector class that waits for notification and then update
221 * the display objects.
222 *
223 * @author imi
224 */
225 private final class Collector implements Runnable {
226 /**
227 * the mouse position of the previous iteration. This is used to show
228 * the popup until the cursor is moved.
229 */
230 private Point oldMousePos;
231 /**
232 * Contains the labels that are currently shown in the information
233 * popup
234 */
235 private List<JLabel> popupLabels = null;
236 /**
237 * The popup displayed to show additional information
238 */
239 private Popup popup;
240
241 private MapFrame parent;
242
243 public Collector(MapFrame parent) {
244 this.parent = parent;
245 }
246
247 /**
248 * Execution function for the Collector.
249 */
250 @Override
251 public void run() {
252 registerListeners();
253 try {
254 for (;;) {
255
256 final MouseState ms = new MouseState();
257 synchronized (this) {
258 // TODO Would be better if the timeout wasn't necessary
259 try {
260 wait(1000);
261 } catch (InterruptedException e) {
262 // Occurs frequently during JOSM shutdown, log set to trace only
263 Main.trace("InterruptedException in "+MapStatus.class.getSimpleName());
264 }
265 ms.modifiers = mouseState.modifiers;
266 ms.mousePos = mouseState.mousePos;
267 }
268 if (parent != Main.map)
269 return; // exit, if new parent.
270
271 // Do nothing, if required data is missing
272 if(ms.mousePos == null || mv.center == null) {
273 continue;
274 }
275
276 try {
277 EventQueue.invokeAndWait(new Runnable() {
278
279 @Override
280 public void run() {
281 // Freeze display when holding down CTRL
282 if ((ms.modifiers & MouseEvent.CTRL_DOWN_MASK) != 0) {
283 // update the information popup's labels though, because
284 // the selection might have changed from the outside
285 popupUpdateLabels();
286 return;
287 }
288
289 // This try/catch is a hack to stop the flooding bug reports about this.
290 // The exception needed to handle with in the first place, means that this
291 // access to the data need to be restarted, if the main thread modifies
292 // the data.
293 DataSet ds = null;
294 // The popup != null check is required because a left-click
295 // produces several events as well, which would make this
296 // variable true. Of course we only want the popup to show
297 // if the middle mouse button has been pressed in the first
298 // place
299 boolean mouseNotMoved = oldMousePos != null
300 && oldMousePos.equals(ms.mousePos);
301 boolean isAtOldPosition = mouseNotMoved && popup != null;
302 boolean middleMouseDown = (ms.modifiers & MouseEvent.BUTTON2_DOWN_MASK) != 0;
303 try {
304 ds = mv.getCurrentDataSet();
305 if (ds != null) {
306 // This is not perfect, if current dataset was changed during execution, the lock would be useless
307 if(isAtOldPosition && middleMouseDown) {
308 // Write lock is necessary when selecting in popupCycleSelection
309 // locks can not be upgraded -> if do read lock here and write lock later (in OsmPrimitive.updateFlags)
310 // then always occurs deadlock (#5814)
311 ds.beginUpdate();
312 } else {
313 ds.getReadLock().lock();
314 }
315 }
316
317 // Set the text label in the bottom status bar
318 // "if mouse moved only" was added to stop heap growing
319 if (!mouseNotMoved) {
320 statusBarElementUpdate(ms);
321 }
322
323 // Popup Information
324 // display them if the middle mouse button is pressed and
325 // keep them until the mouse is moved
326 if (middleMouseDown || isAtOldPosition) {
327 Collection<OsmPrimitive> osms = mv.getAllNearest(ms.mousePos, OsmPrimitive.isUsablePredicate);
328
329 final JPanel c = new JPanel(new GridBagLayout());
330 final JLabel lbl = new JLabel(
331 "<html>"+tr("Middle click again to cycle through.<br>"+
332 "Hold CTRL to select directly from this list with the mouse.<hr>")+"</html>",
333 null,
334 JLabel.HORIZONTAL
335 );
336 lbl.setHorizontalAlignment(JLabel.LEFT);
337 c.add(lbl, GBC.eol().insets(2, 0, 2, 0));
338
339 // Only cycle if the mouse has not been moved and the
340 // middle mouse button has been pressed at least twice
341 // (the reason for this is the popup != null check for
342 // isAtOldPosition, see above. This is a nice side
343 // effect though, because it does not change selection
344 // of the first middle click)
345 if(isAtOldPosition && middleMouseDown) {
346 // Hand down mouse modifiers so the SHIFT mod can be
347 // handled correctly (see funcion)
348 popupCycleSelection(osms, ms.modifiers);
349 }
350
351 // These labels may need to be updated from the outside
352 // so collect them
353 List<JLabel> lbls = new ArrayList<JLabel>(osms.size());
354 for (final OsmPrimitive osm : osms) {
355 JLabel l = popupBuildPrimitiveLabels(osm);
356 lbls.add(l);
357 c.add(l, GBC.eol().fill(GBC.HORIZONTAL).insets(2, 0, 2, 2));
358 }
359
360 popupShowPopup(popupCreatePopup(c, ms), lbls);
361 } else {
362 popupHidePopup();
363 }
364
365 oldMousePos = ms.mousePos;
366 } catch (ConcurrentModificationException x) {
367 Main.warn(x);
368 } finally {
369 if (ds != null) {
370 if(isAtOldPosition && middleMouseDown) {
371 ds.endUpdate();
372 } else {
373 ds.getReadLock().unlock();
374 }
375 }
376 }
377 }
378 });
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
396 * @param ms
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
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.addSelected(firstItem);
476 } else {
477 ds.clearSelection(firstSelected);
478 if(nextSelected != null) {
479 ds.addSelected(nextSelected);
480 }
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 * Tries to show the given popup, can be hidden using {@link #popupHidePopup}
502 * If an old popup exists, it will be automatically hidden
503 * @param newPopup popup to show
504 * @param lbls lables to show (see {@link #popupLabels})
505 */
506 private void popupShowPopup(Popup newPopup, List<JLabel> lbls) {
507 final Popup staticPopup = newPopup;
508 if(this.popup != null) {
509 // If an old popup exists, remove it when the new popup has been
510 // drawn to keep flickering to a minimum
511 final Popup staticOldPopup = this.popup;
512 EventQueue.invokeLater(new Runnable(){
513 @Override public void run() {
514 staticPopup.show();
515 staticOldPopup.hide();
516 }
517 });
518 } else {
519 // There is no old popup
520 EventQueue.invokeLater(new Runnable(){
521 @Override public void run() { staticPopup.show(); }});
522 }
523 this.popupLabels = lbls;
524 this.popup = newPopup;
525 }
526
527 /**
528 * This method should be called if the selection may have changed from
529 * outside of this class. This is the case when CTRL is pressed and the
530 * user clicks on the map instead of the popup.
531 */
532 private void popupUpdateLabels() {
533 if(this.popup == null || this.popupLabels == null)
534 return;
535 for(JLabel l : this.popupLabels) {
536 l.validate();
537 }
538 }
539
540 /**
541 * Sets the colors for the given label depending on the selected status of
542 * the given OsmPrimitive
543 *
544 * @param lbl The label to color
545 * @param osm The primitive to derive the colors from
546 */
547 private void popupSetLabelColors(JLabel lbl, OsmPrimitive osm) {
548 DataSet ds = Main.main.getCurrentDataSet();
549 if(ds.isSelected(osm)) {
550 lbl.setBackground(SystemColor.textHighlight);
551 lbl.setForeground(SystemColor.textHighlightText);
552 } else {
553 lbl.setBackground(SystemColor.control);
554 lbl.setForeground(SystemColor.controlText);
555 }
556 }
557
558 /**
559 * Builds the labels with all necessary listeners for the info popup for the
560 * given OsmPrimitive
561 * @param osm The primitive to create the label for
562 * @return labels for info popup
563 */
564 private JLabel popupBuildPrimitiveLabels(final OsmPrimitive osm) {
565 final StringBuilder text = new StringBuilder();
566 String name = osm.getDisplayName(DefaultNameFormatter.getInstance());
567 if (osm.isNewOrUndeleted() || osm.isModified()) {
568 name = "<i><b>"+ name + "*</b></i>";
569 }
570 text.append(name);
571
572 boolean idShown = Main.pref.getBoolean("osm-primitives.showid");
573 // fix #7557 - do not show ID twice
574
575 if (!osm.isNew() && !idShown) {
576 text.append(" [id="+osm.getId()+"]");
577 }
578
579 if(osm.getUser() != null) {
580 text.append(" [" + tr("User:") + " " + osm.getUser().getName() + "]");
581 }
582
583 for (String key : osm.keySet()) {
584 text.append("<br>" + key + "=" + osm.get(key));
585 }
586
587 final JLabel l = new JLabel(
588 "<html>" +text.toString() + "</html>",
589 ImageProvider.get(osm.getDisplayType()),
590 JLabel.HORIZONTAL
591 ) {
592 // This is necessary so the label updates its colors when the
593 // selection is changed from the outside
594 @Override public void validate() {
595 super.validate();
596 popupSetLabelColors(this, osm);
597 }
598 };
599 l.setOpaque(true);
600 popupSetLabelColors(l, osm);
601 l.setFont(l.getFont().deriveFont(Font.PLAIN));
602 l.setVerticalTextPosition(JLabel.TOP);
603 l.setHorizontalAlignment(JLabel.LEFT);
604 l.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
605 l.addMouseListener(new MouseAdapter(){
606 @Override public void mouseEntered(MouseEvent e) {
607 l.setBackground(SystemColor.info);
608 l.setForeground(SystemColor.infoText);
609 }
610 @Override public void mouseExited(MouseEvent e) {
611 popupSetLabelColors(l, osm);
612 }
613 @Override public void mouseClicked(MouseEvent e) {
614 DataSet ds = Main.main.getCurrentDataSet();
615 // Let the user toggle the selection
616 ds.toggleSelected(osm);
617 l.validate();
618 }
619 });
620 // Sometimes the mouseEntered event is not catched, thus the label
621 // will not be highlighted, making it confusing. The MotionListener
622 // can correct this defect.
623 l.addMouseMotionListener(new MouseMotionListener() {
624 @Override public void mouseMoved(MouseEvent e) {
625 l.setBackground(SystemColor.info);
626 l.setForeground(SystemColor.infoText);
627 }
628 @Override public void mouseDragged(MouseEvent e) {
629 l.setBackground(SystemColor.info);
630 l.setForeground(SystemColor.infoText);
631 }
632 });
633 return l;
634 }
635 }
636
637 /**
638 * Everything, the collector is interested of. Access must be synchronized.
639 * @author imi
640 */
641 static class MouseState {
642 Point mousePos;
643 int modifiers;
644 }
645 /**
646 * The last sent mouse movement event.
647 */
648 MouseState mouseState = new MouseState();
649
650 private AWTEventListener awtListener = new AWTEventListener() {
651 @Override
652 public void eventDispatched(AWTEvent event) {
653 if (event instanceof InputEvent &&
654 ((InputEvent)event).getComponent() == mv) {
655 synchronized (collector) {
656 mouseState.modifiers = ((InputEvent)event).getModifiersEx();
657 if (event instanceof MouseEvent) {
658 mouseState.mousePos = ((MouseEvent)event).getPoint();
659 }
660 collector.notify();
661 }
662 }
663 }
664 };
665
666 private MouseMotionListener mouseMotionListener = new MouseMotionListener() {
667 @Override
668 public void mouseMoved(MouseEvent e) {
669 synchronized (collector) {
670 mouseState.modifiers = e.getModifiersEx();
671 mouseState.mousePos = e.getPoint();
672 collector.notify();
673 }
674 }
675
676 @Override
677 public void mouseDragged(MouseEvent e) {
678 mouseMoved(e);
679 }
680 };
681
682 private KeyAdapter keyAdapter = new KeyAdapter() {
683 @Override public void keyPressed(KeyEvent e) {
684 synchronized (collector) {
685 mouseState.modifiers = e.getModifiersEx();
686 collector.notify();
687 }
688 }
689
690 @Override public void keyReleased(KeyEvent e) {
691 keyPressed(e);
692 }
693 };
694
695 private void registerListeners() {
696 // Listen to keyboard/mouse events for pressing/releasing alt key and
697 // inform the collector.
698 try {
699 Toolkit.getDefaultToolkit().addAWTEventListener(awtListener,
700 AWTEvent.KEY_EVENT_MASK | AWTEvent.MOUSE_EVENT_MASK | AWTEvent.MOUSE_MOTION_EVENT_MASK);
701 } catch (SecurityException ex) {
702 mv.addMouseMotionListener(mouseMotionListener);
703 mv.addKeyListener(keyAdapter);
704 }
705 }
706
707 private void unregisterListeners() {
708 try {
709 Toolkit.getDefaultToolkit().removeAWTEventListener(awtListener);
710 } catch (SecurityException e) {
711 // Don't care, awtListener probably wasn't registered anyway
712 }
713 mv.removeMouseMotionListener(mouseMotionListener);
714 mv.removeKeyListener(keyAdapter);
715 }
716
717 private class MapStatusPopupMenu extends JPopupMenu {
718
719 private final JMenuItem jumpButton = createActionComponent(Main.main.menu.jumpToAct);
720
721 private final Collection<JCheckBoxMenuItem> somItems = new ArrayList<JCheckBoxMenuItem>();
722
723 private final JSeparator separator = new JSeparator();
724
725 private final JMenuItem doNotHide = new JCheckBoxMenuItem(new AbstractAction(tr("Do not hide status bar")) {
726 @Override
727 public void actionPerformed(ActionEvent e) {
728 boolean sel = ((JCheckBoxMenuItem) e.getSource()).getState();
729 Main.pref.put("statusbar.always-visible", sel);
730 }
731 });
732
733 public MapStatusPopupMenu() {
734 for (final String key : new TreeSet<String>(NavigatableComponent.SYSTEMS_OF_MEASUREMENT.keySet())) {
735 JCheckBoxMenuItem item = new JCheckBoxMenuItem(new AbstractAction(key) {
736 @Override
737 public void actionPerformed(ActionEvent e) {
738 updateSystemOfMeasurement(key);
739 }
740 });
741 somItems.add(item);
742 add(item);
743 }
744
745 add(jumpButton);
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(invoker == latText || invoker == lonText);
754 String currentSOM = ProjectionPreference.PROP_SYSTEM_OF_MEASUREMENT.get();;
755 for (JMenuItem item : somItems) {
756 item.setSelected(item.getText().equals(currentSOM));
757 item.setVisible(invoker == distText);
758 }
759 separator.setVisible(invoker == distText);
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<String>(new TreeSet<String>(NavigatableComponent.SYSTEMS_OF_MEASUREMENT.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.