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

Last change on this file since 12841 was 12841, checked in by bastiK, 6 weeks ago

see #15229 - fix deprecations caused by [12840]

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