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

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

sonar - squid:S2142 - "InterruptedException" should not be ignored

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