001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.awt.BorderLayout;
007import java.awt.Component;
008import java.awt.Container;
009import java.awt.Dimension;
010import java.awt.Font;
011import java.awt.GridBagLayout;
012import java.awt.Rectangle;
013import java.awt.event.ActionEvent;
014import java.awt.event.KeyEvent;
015import java.util.ArrayList;
016import java.util.Collection;
017import java.util.HashMap;
018import java.util.List;
019import java.util.Map;
020import java.util.concurrent.CopyOnWriteArrayList;
021
022import javax.swing.AbstractAction;
023import javax.swing.AbstractButton;
024import javax.swing.Action;
025import javax.swing.BorderFactory;
026import javax.swing.BoxLayout;
027import javax.swing.ButtonGroup;
028import javax.swing.ImageIcon;
029import javax.swing.InputMap;
030import javax.swing.JButton;
031import javax.swing.JCheckBoxMenuItem;
032import javax.swing.JComponent;
033import javax.swing.JPanel;
034import javax.swing.JPopupMenu;
035import javax.swing.JSplitPane;
036import javax.swing.JToggleButton;
037import javax.swing.JToolBar;
038import javax.swing.KeyStroke;
039import javax.swing.border.Border;
040import javax.swing.event.PopupMenuEvent;
041import javax.swing.event.PopupMenuListener;
042import javax.swing.plaf.basic.BasicSplitPaneDivider;
043import javax.swing.plaf.basic.BasicSplitPaneUI;
044
045import org.openstreetmap.josm.actions.LassoModeAction;
046import org.openstreetmap.josm.actions.mapmode.DeleteAction;
047import org.openstreetmap.josm.actions.mapmode.DrawAction;
048import org.openstreetmap.josm.actions.mapmode.ExtrudeAction;
049import org.openstreetmap.josm.actions.mapmode.ImproveWayAccuracyAction;
050import org.openstreetmap.josm.actions.mapmode.MapMode;
051import org.openstreetmap.josm.actions.mapmode.ParallelWayAction;
052import org.openstreetmap.josm.actions.mapmode.SelectAction;
053import org.openstreetmap.josm.actions.mapmode.ZoomAction;
054import org.openstreetmap.josm.data.ViewportData;
055import org.openstreetmap.josm.data.preferences.BooleanProperty;
056import org.openstreetmap.josm.data.preferences.IntegerProperty;
057import org.openstreetmap.josm.gui.dialogs.ChangesetDialog;
058import org.openstreetmap.josm.gui.dialogs.CommandStackDialog;
059import org.openstreetmap.josm.gui.dialogs.ConflictDialog;
060import org.openstreetmap.josm.gui.dialogs.DialogsPanel;
061import org.openstreetmap.josm.gui.dialogs.FilterDialog;
062import org.openstreetmap.josm.gui.dialogs.LayerListDialog;
063import org.openstreetmap.josm.gui.dialogs.MapPaintDialog;
064import org.openstreetmap.josm.gui.dialogs.MinimapDialog;
065import org.openstreetmap.josm.gui.dialogs.NotesDialog;
066import org.openstreetmap.josm.gui.dialogs.RelationListDialog;
067import org.openstreetmap.josm.gui.dialogs.SelectionListDialog;
068import org.openstreetmap.josm.gui.dialogs.ToggleDialog;
069import org.openstreetmap.josm.gui.dialogs.UserListDialog;
070import org.openstreetmap.josm.gui.dialogs.ValidatorDialog;
071import org.openstreetmap.josm.gui.dialogs.properties.PropertiesDialog;
072import org.openstreetmap.josm.gui.layer.Layer;
073import org.openstreetmap.josm.gui.layer.LayerManager.LayerAddEvent;
074import org.openstreetmap.josm.gui.layer.LayerManager.LayerChangeListener;
075import org.openstreetmap.josm.gui.layer.LayerManager.LayerOrderChangeEvent;
076import org.openstreetmap.josm.gui.layer.LayerManager.LayerRemoveEvent;
077import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeEvent;
078import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeListener;
079import org.openstreetmap.josm.gui.util.AdvancedKeyPressDetector;
080import org.openstreetmap.josm.spi.preferences.Config;
081import org.openstreetmap.josm.spi.preferences.PreferenceChangedListener;
082import org.openstreetmap.josm.tools.Destroyable;
083import org.openstreetmap.josm.tools.GBC;
084import org.openstreetmap.josm.tools.ImageProvider;
085import org.openstreetmap.josm.tools.Shortcut;
086
087/**
088 * One Map frame with one dataset behind. This is the container gui class whose
089 * display can be set to the different views.
090 *
091 * @author imi
092 */
093public class MapFrame extends JPanel implements Destroyable, ActiveLayerChangeListener, LayerChangeListener {
094    /**
095     * Default width of the toggle dialog area.
096     */
097    public static final int DEF_TOGGLE_DLG_WIDTH = 330;
098
099    private static final IntegerProperty TOGGLE_DIALOGS_WIDTH = new IntegerProperty("toggleDialogs.width", DEF_TOGGLE_DLG_WIDTH);
100    /**
101     * Do not require to switch modes (potlatch style workflow) for drawing/selecting map modes.
102     * @since 12347
103     */
104    public static final BooleanProperty MODELESS = new BooleanProperty("modeless", false);
105    /**
106     * The current mode, this frame operates.
107     */
108    public MapMode mapMode;
109
110    /**
111     * The view control displayed.
112     */
113    public final MapView mapView;
114
115    /**
116     * This object allows to detect key press and release events
117     */
118    public final transient AdvancedKeyPressDetector keyDetector = new AdvancedKeyPressDetector();
119
120    /**
121     * The toolbar with the action icons. To add new toggle dialog buttons,
122     * use addToggleDialog, to add a new map mode button use addMapMode.
123     */
124    private JComponent sideToolBar = new JToolBar(JToolBar.VERTICAL);
125    private final ButtonGroup toolBarActionsGroup = new ButtonGroup();
126    private final JToolBar toolBarActions = new JToolBar(JToolBar.VERTICAL);
127    private final JToolBar toolBarToggle = new JToolBar(JToolBar.VERTICAL);
128
129    private final List<ToggleDialog> allDialogs = new ArrayList<>();
130    private final List<IconToggleButton> allDialogButtons = new ArrayList<>();
131    /**
132     * All map mode buttons. Should only be read form the outside
133     */
134    public final List<IconToggleButton> allMapModeButtons = new ArrayList<>();
135
136    private final ListAllButtonsAction listAllDialogsAction = new ListAllButtonsAction(allDialogButtons);
137    private final ListAllButtonsAction listAllMapModesAction = new ListAllButtonsAction(allMapModeButtons);
138    private final JButton listAllToggleDialogsButton = new JButton(listAllDialogsAction);
139    private final JButton listAllMapModesButton = new JButton(listAllMapModesAction);
140
141    {
142        listAllDialogsAction.setButton(listAllToggleDialogsButton);
143        listAllMapModesAction.setButton(listAllMapModesButton);
144    }
145
146    // Toggle dialogs
147
148    /** Conflict dialog */
149    public final ConflictDialog conflictDialog;
150    /** Filter dialog */
151    public final FilterDialog filterDialog;
152    /** Relation list dialog */
153    public final RelationListDialog relationListDialog;
154    /** Validator dialog */
155    public final ValidatorDialog validatorDialog;
156    /** Selection list dialog */
157    public final SelectionListDialog selectionListDialog;
158    /** Properties dialog */
159    public final PropertiesDialog propertiesDialog;
160    /** Map paint dialog */
161    public final MapPaintDialog mapPaintDialog;
162    /** Notes dialog */
163    public final NotesDialog noteDialog;
164
165    // Map modes
166
167    /** Select mode */
168    public final SelectAction mapModeSelect;
169    /** Draw mode */
170    public final DrawAction mapModeDraw;
171    /** Zoom mode */
172    public final ZoomAction mapModeZoom;
173    /** Delete mode */
174    public final DeleteAction mapModeDelete;
175    /** Select Lasso mode */
176    public LassoModeAction mapModeSelectLasso;
177
178    private final transient Map<Layer, MapMode> lastMapMode = new HashMap<>();
179
180    /**
181     * The status line below the map
182     */
183    public MapStatus statusLine;
184
185    /**
186     * The split pane with the mapview (leftPanel) and toggle dialogs (dialogsPanel).
187     */
188    private final JSplitPane splitPane;
189    private final JPanel leftPanel;
190    private final DialogsPanel dialogsPanel;
191
192    /**
193     * Constructs a new {@code MapFrame}.
194     * @param viewportData the initial viewport of the map. Can be null, then
195     * the viewport is derived from the layer data.
196     * @since 11713
197     */
198    public MapFrame(ViewportData viewportData) {
199        setSize(400, 400);
200        setLayout(new BorderLayout());
201
202        mapView = new MapView(MainApplication.getLayerManager(), viewportData);
203
204        splitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, true);
205
206        leftPanel = new JPanel(new GridBagLayout());
207        leftPanel.add(mapView, GBC.std().fill());
208        splitPane.setLeftComponent(leftPanel);
209
210        dialogsPanel = new DialogsPanel(splitPane);
211        splitPane.setRightComponent(dialogsPanel);
212
213        /**
214         * All additional space goes to the mapView
215         */
216        splitPane.setResizeWeight(1.0);
217
218        /**
219         * Some beautifications.
220         */
221        splitPane.setDividerSize(5);
222        splitPane.setBorder(null);
223        splitPane.setUI(new NoBorderSplitPaneUI());
224
225        // JSplitPane supports F6, F8, Home and End shortcuts by default, but we need them for Audio and Image Mapping actions
226        InputMap splitInputMap = splitPane.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
227        splitInputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_F6, 0), new Object());
228        splitInputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_F8, 0), new Object());
229        splitInputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_HOME, 0), new Object());
230        splitInputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_END, 0), new Object());
231
232        add(splitPane, BorderLayout.CENTER);
233
234        dialogsPanel.setLayout(new BoxLayout(dialogsPanel, BoxLayout.Y_AXIS));
235        dialogsPanel.setPreferredSize(new Dimension(TOGGLE_DIALOGS_WIDTH.get(), 0));
236        dialogsPanel.setMinimumSize(new Dimension(24, 0));
237        mapView.setMinimumSize(new Dimension(10, 0));
238
239        // toolBarActions, map mode buttons
240        mapModeSelect = new SelectAction(this);
241        mapModeSelectLasso = new LassoModeAction();
242        mapModeDraw = new DrawAction();
243        mapModeZoom = new ZoomAction(this);
244        mapModeDelete = new DeleteAction();
245
246        addMapMode(new IconToggleButton(mapModeSelect));
247        addMapMode(new IconToggleButton(mapModeSelectLasso, true));
248        addMapMode(new IconToggleButton(mapModeDraw));
249        addMapMode(new IconToggleButton(mapModeZoom, true));
250        addMapMode(new IconToggleButton(mapModeDelete, true));
251        addMapMode(new IconToggleButton(new ParallelWayAction(this), true));
252        addMapMode(new IconToggleButton(new ExtrudeAction(), true));
253        addMapMode(new IconToggleButton(new ImproveWayAccuracyAction(), false));
254        toolBarActionsGroup.setSelected(allMapModeButtons.get(0).getModel(), true);
255        toolBarActions.setFloatable(false);
256
257        // toolBarToggles, toggle dialog buttons
258        LayerListDialog.createInstance(mapView.getLayerManager());
259        propertiesDialog = new PropertiesDialog();
260        selectionListDialog = new SelectionListDialog();
261        relationListDialog = new RelationListDialog();
262        conflictDialog = new ConflictDialog();
263        validatorDialog = new ValidatorDialog();
264        filterDialog = new FilterDialog();
265        mapPaintDialog = new MapPaintDialog();
266        noteDialog = new NotesDialog();
267
268        addToggleDialog(LayerListDialog.getInstance());
269        addToggleDialog(propertiesDialog);
270        addToggleDialog(selectionListDialog);
271        addToggleDialog(relationListDialog);
272        addToggleDialog(new MinimapDialog());
273        addToggleDialog(new CommandStackDialog());
274        addToggleDialog(new UserListDialog());
275        addToggleDialog(conflictDialog);
276        addToggleDialog(validatorDialog);
277        addToggleDialog(filterDialog);
278        addToggleDialog(new ChangesetDialog(), true);
279        addToggleDialog(mapPaintDialog);
280        addToggleDialog(noteDialog);
281        toolBarToggle.setFloatable(false);
282
283        // status line below the map
284        statusLine = new MapStatus(this);
285        MainApplication.getLayerManager().addLayerChangeListener(this);
286        MainApplication.getLayerManager().addActiveLayerChangeListener(this);
287
288        boolean unregisterTab = Shortcut.findShortcut(KeyEvent.VK_TAB, 0).isPresent();
289        if (unregisterTab) {
290            for (JComponent c: allDialogButtons) {
291                c.setFocusTraversalKeysEnabled(false);
292            }
293            for (JComponent c: allMapModeButtons) {
294                c.setFocusTraversalKeysEnabled(false);
295            }
296        }
297
298        if (Config.getPref().getBoolean("debug.advanced-keypress-detector.enable", true)) {
299            keyDetector.register();
300        }
301    }
302
303    /**
304     * Enables the select tool
305     * @param onlyIfModeless Only enable if modeless mode is active
306     * @return <code>true</code> if it is selected
307     */
308    public boolean selectSelectTool(boolean onlyIfModeless) {
309        if (onlyIfModeless && !MODELESS.get())
310            return false;
311
312        return selectMapMode(mapModeSelect);
313    }
314
315    /**
316     * Enables the draw tool
317     * @param onlyIfModeless Only enable if modeless mode is active
318     * @return <code>true</code> if it is selected
319     */
320    public boolean selectDrawTool(boolean onlyIfModeless) {
321        if (onlyIfModeless && !MODELESS.get())
322            return false;
323
324        return selectMapMode(mapModeDraw);
325    }
326
327    /**
328     * Enables the zoom tool
329     * @param onlyIfModeless Only enable if modeless mode is active
330     * @return <code>true</code> if it is selected
331     */
332    public boolean selectZoomTool(boolean onlyIfModeless) {
333        if (onlyIfModeless && !MODELESS.get())
334            return false;
335
336        return selectMapMode(mapModeZoom);
337    }
338
339    /**
340     * Called as some kind of destructor when the last layer has been removed.
341     * Delegates the call to all Destroyables within this component (e.g. MapModes)
342     */
343    @Override
344    public void destroy() {
345        MainApplication.getLayerManager().removeLayerChangeListener(this);
346        MainApplication.getLayerManager().removeActiveLayerChangeListener(this);
347        dialogsPanel.destroy();
348        Config.getPref().removePreferenceChangeListener(sidetoolbarPreferencesChangedListener);
349        for (int i = 0; i < toolBarActions.getComponentCount(); ++i) {
350            if (toolBarActions.getComponent(i) instanceof Destroyable) {
351                ((Destroyable) toolBarActions.getComponent(i)).destroy();
352            }
353        }
354        toolBarActions.removeAll();
355        for (int i = 0; i < toolBarToggle.getComponentCount(); ++i) {
356            if (toolBarToggle.getComponent(i) instanceof Destroyable) {
357                ((Destroyable) toolBarToggle.getComponent(i)).destroy();
358            }
359        }
360        toolBarToggle.removeAll();
361
362        statusLine.destroy();
363        mapView.destroy();
364        keyDetector.unregister();
365
366        allDialogs.clear();
367        allDialogButtons.clear();
368        allMapModeButtons.clear();
369    }
370
371    /**
372     * Gets the action of the default (first) map mode
373     * @return That action
374     */
375    public Action getDefaultButtonAction() {
376        return ((AbstractButton) toolBarActions.getComponent(0)).getAction();
377    }
378
379    /**
380     * Open all ToggleDialogs that have their preferences property set. Close all others.
381     */
382    public void initializeDialogsPane() {
383        dialogsPanel.initialize(allDialogs);
384    }
385
386    /**
387     * Adds a new toggle dialog to the left button list. It is displayed in expert and normal mode
388     * @param dlg The dialog
389     * @return The button
390     */
391    public IconToggleButton addToggleDialog(final ToggleDialog dlg) {
392        return addToggleDialog(dlg, false);
393    }
394
395    /**
396     * Call this to add new toggle dialogs to the left button-list
397     * @param dlg The toggle dialog. It must not be in the list already.
398     * @param isExpert {@code true} if it's reserved to expert mode
399     * @return button allowing to toggle the dialog
400     */
401    public IconToggleButton addToggleDialog(final ToggleDialog dlg, boolean isExpert) {
402        final IconToggleButton button = new IconToggleButton(dlg.getToggleAction(), isExpert);
403        button.setShowHideButtonListener(dlg);
404        button.setInheritsPopupMenu(true);
405        dlg.setButton(button);
406        toolBarToggle.add(button);
407        allDialogs.add(dlg);
408        allDialogButtons.add(button);
409        button.applyButtonHiddenPreferences();
410        if (dialogsPanel.initialized) {
411            dialogsPanel.add(dlg);
412        }
413        return button;
414    }
415
416    /**
417     * Call this to remove existing toggle dialog from the left button-list
418     * @param dlg The toggle dialog. It must be already in the list.
419     * @since 10851
420     */
421    public void removeToggleDialog(final ToggleDialog dlg) {
422        final JToggleButton button = dlg.getButton();
423        if (button != null) {
424            allDialogButtons.remove(button);
425            toolBarToggle.remove(button);
426        }
427        dialogsPanel.remove(dlg);
428        allDialogs.remove(dlg);
429    }
430
431    /**
432     * Adds a new map mode button
433     * @param b The map mode button with a {@link MapMode} action.
434     */
435    public void addMapMode(IconToggleButton b) {
436        if (!(b.getAction() instanceof MapMode))
437            throw new IllegalArgumentException("MapMode action must be subclass of MapMode");
438        allMapModeButtons.add(b);
439        toolBarActionsGroup.add(b);
440        toolBarActions.add(b);
441        b.applyButtonHiddenPreferences();
442        b.setInheritsPopupMenu(true);
443    }
444
445    /**
446     * Fires an property changed event "visible".
447     * @param aFlag {@code true} if display should be visible
448     */
449    @Override public void setVisible(boolean aFlag) {
450        boolean old = isVisible();
451        super.setVisible(aFlag);
452        if (old != aFlag) {
453            firePropertyChange("visible", old, aFlag);
454        }
455    }
456
457    /**
458     * Change the operating map mode for the view. Will call unregister on the
459     * old MapMode and register on the new one. Now this function also verifies
460     * if new map mode is correct mode for current layer and does not change mode
461     * in such cases.
462     * @param newMapMode The new mode to set.
463     * @return {@code true} if mode is really selected
464     */
465    public boolean selectMapMode(MapMode newMapMode) {
466        return selectMapMode(newMapMode, mapView.getLayerManager().getActiveLayer());
467    }
468
469    /**
470     * Another version of the selectMapMode for changing layer action.
471     * Pass newly selected layer to this method.
472     * @param newMapMode The new mode to set.
473     * @param newLayer newly selected layer
474     * @return {@code true} if mode is really selected
475     */
476    public boolean selectMapMode(MapMode newMapMode, Layer newLayer) {
477        MapMode oldMapMode = this.mapMode;
478        if (newMapMode == oldMapMode)
479            return true;
480        if (newMapMode == null || !newMapMode.layerIsSupported(newLayer)) {
481            newMapMode = null;
482        }
483
484        if (oldMapMode != null) {
485            oldMapMode.exitMode();
486        }
487        this.mapMode = newMapMode;
488        if (newMapMode != null) {
489            newMapMode.enterMode();
490        }
491        lastMapMode.put(newLayer, newMapMode);
492        fireMapModeChanged(oldMapMode, newMapMode);
493        return newMapMode != null;
494    }
495
496    /**
497     * Fill the given panel by adding all necessary components to the different
498     * locations.
499     *
500     * @param panel The container to fill. Must have a BorderLayout.
501     */
502    public void fillPanel(Container panel) {
503        panel.add(this, BorderLayout.CENTER);
504
505        /**
506         * sideToolBar: add map modes icons
507         */
508        if (Config.getPref().getBoolean("sidetoolbar.mapmodes.visible", true)) {
509            toolBarActions.setAlignmentX(0.5f);
510            toolBarActions.setBorder(null);
511            toolBarActions.setInheritsPopupMenu(true);
512            sideToolBar.add(toolBarActions);
513            listAllMapModesButton.setAlignmentX(0.5f);
514            listAllMapModesButton.setBorder(null);
515            listAllMapModesButton.setFont(listAllMapModesButton.getFont().deriveFont(Font.PLAIN));
516            listAllMapModesButton.setInheritsPopupMenu(true);
517            sideToolBar.add(listAllMapModesButton);
518        }
519
520        /**
521         * sideToolBar: add toggle dialogs icons
522         */
523        if (Config.getPref().getBoolean("sidetoolbar.toggledialogs.visible", true)) {
524            ((JToolBar) sideToolBar).addSeparator(new Dimension(0, 18));
525            toolBarToggle.setAlignmentX(0.5f);
526            toolBarToggle.setBorder(null);
527            toolBarToggle.setInheritsPopupMenu(true);
528            sideToolBar.add(toolBarToggle);
529            listAllToggleDialogsButton.setAlignmentX(0.5f);
530            listAllToggleDialogsButton.setBorder(null);
531            listAllToggleDialogsButton.setFont(listAllToggleDialogsButton.getFont().deriveFont(Font.PLAIN));
532            listAllToggleDialogsButton.setInheritsPopupMenu(true);
533            sideToolBar.add(listAllToggleDialogsButton);
534        }
535
536        /**
537         * sideToolBar: add dynamic popup menu
538         */
539        sideToolBar.setComponentPopupMenu(new SideToolbarPopupMenu());
540        ((JToolBar) sideToolBar).setFloatable(false);
541        sideToolBar.setBorder(BorderFactory.createEmptyBorder(0, 1, 0, 1));
542
543        /**
544         * sideToolBar: decide scroll- and visibility
545         */
546        if (Config.getPref().getBoolean("sidetoolbar.scrollable", true)) {
547            final ScrollViewport svp = new ScrollViewport(sideToolBar, ScrollViewport.VERTICAL_DIRECTION);
548            sideToolBar = svp;
549        }
550        sideToolBar.setVisible(Config.getPref().getBoolean("sidetoolbar.visible", true));
551        sidetoolbarPreferencesChangedListener = e -> {
552            if ("sidetoolbar.visible".equals(e.getKey())) {
553                sideToolBar.setVisible(Config.getPref().getBoolean("sidetoolbar.visible"));
554            }
555        };
556        Config.getPref().addPreferenceChangeListener(sidetoolbarPreferencesChangedListener);
557
558        /**
559         * sideToolBar: add it to the panel
560         */
561        panel.add(sideToolBar, BorderLayout.WEST);
562
563        /**
564         * statusLine: add to panel
565         */
566        if (statusLine != null && Config.getPref().getBoolean("statusline.visible", true)) {
567            panel.add(statusLine, BorderLayout.SOUTH);
568        }
569    }
570
571    static final class NoBorderSplitPaneUI extends BasicSplitPaneUI {
572        static final class NoBorderBasicSplitPaneDivider extends BasicSplitPaneDivider {
573            NoBorderBasicSplitPaneDivider(BasicSplitPaneUI ui) {
574                super(ui);
575            }
576
577            @Override
578            public void setBorder(Border b) {
579                // Do nothing
580            }
581        }
582
583        @Override
584        public BasicSplitPaneDivider createDefaultDivider() {
585            return new NoBorderBasicSplitPaneDivider(this);
586        }
587    }
588
589    private final class SideToolbarPopupMenu extends JPopupMenu {
590        private static final int staticMenuEntryCount = 2;
591        private final JCheckBoxMenuItem doNotHide = new JCheckBoxMenuItem(new AbstractAction(tr("Do not hide toolbar")) {
592            @Override
593            public void actionPerformed(ActionEvent e) {
594                boolean sel = ((JCheckBoxMenuItem) e.getSource()).getState();
595                Config.getPref().putBoolean("sidetoolbar.always-visible", sel);
596            }
597        });
598        {
599            addPopupMenuListener(new PopupMenuListener() {
600                @Override
601                public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
602                    final Object src = ((JPopupMenu) e.getSource()).getInvoker();
603                    if (src instanceof IconToggleButton) {
604                        insert(new Separator(), 0);
605                        insert(new AbstractAction() {
606                            {
607                                putValue(NAME, tr("Hide this button"));
608                                putValue(SHORT_DESCRIPTION, tr("Click the arrow at the bottom to show it again."));
609                            }
610
611                            @Override
612                            public void actionPerformed(ActionEvent e) {
613                                ((IconToggleButton) src).setButtonHidden(true);
614                                validateToolBarsVisibility();
615                            }
616                        }, 0);
617                    }
618                    doNotHide.setSelected(Config.getPref().getBoolean("sidetoolbar.always-visible", true));
619                }
620
621                @Override
622                public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
623                    while (getComponentCount() > staticMenuEntryCount) {
624                        remove(0);
625                    }
626                }
627
628                @Override
629                public void popupMenuCanceled(PopupMenuEvent e) {
630                    // Do nothing
631                }
632            });
633
634            add(new AbstractAction(tr("Hide edit toolbar")) {
635                @Override
636                public void actionPerformed(ActionEvent e) {
637                    Config.getPref().putBoolean("sidetoolbar.visible", false);
638                }
639            });
640            add(doNotHide);
641        }
642    }
643
644    class ListAllButtonsAction extends AbstractAction {
645
646        private JButton button;
647        private final transient Collection<? extends HideableButton> buttons;
648
649        ListAllButtonsAction(Collection<? extends HideableButton> buttons) {
650            this.buttons = buttons;
651        }
652
653        public void setButton(JButton button) {
654            this.button = button;
655            final ImageIcon icon = ImageProvider.get("audio-fwd");
656            putValue(SMALL_ICON, icon);
657            button.setPreferredSize(new Dimension(icon.getIconWidth(), icon.getIconHeight() + 64));
658        }
659
660        @Override
661        public void actionPerformed(ActionEvent e) {
662            JPopupMenu menu = new JPopupMenu();
663            for (HideableButton b : buttons) {
664                final HideableButton t = b;
665                menu.add(new JCheckBoxMenuItem(new AbstractAction() {
666                    {
667                        putValue(NAME, t.getActionName());
668                        putValue(SMALL_ICON, t.getIcon());
669                        putValue(SELECTED_KEY, t.isButtonVisible());
670                        putValue(SHORT_DESCRIPTION, tr("Hide or show this toggle button"));
671                    }
672
673                    @Override
674                    public void actionPerformed(ActionEvent e) {
675                        if ((Boolean) getValue(SELECTED_KEY)) {
676                            t.showButton();
677                        } else {
678                            t.hideButton();
679                        }
680                        validateToolBarsVisibility();
681                    }
682                }));
683            }
684            if (button != null && button.isShowing()) {
685                Rectangle bounds = button.getBounds();
686                menu.show(button, bounds.x + bounds.width, 0);
687            }
688        }
689    }
690
691    /**
692     * Validate the visibility of all tool bars and hide the ones that should be hidden
693     */
694    public void validateToolBarsVisibility() {
695        for (IconToggleButton b : allDialogButtons) {
696            b.applyButtonHiddenPreferences();
697        }
698        toolBarToggle.repaint();
699        for (IconToggleButton b : allMapModeButtons) {
700            b.applyButtonHiddenPreferences();
701        }
702        toolBarActions.repaint();
703    }
704
705    /**
706     * Replies the instance of a toggle dialog of type <code>type</code> managed by this map frame
707     *
708     * @param <T> toggle dialog type
709     * @param type the class of the toggle dialog, i.e. UserListDialog.class
710     * @return the instance of a toggle dialog of type <code>type</code> managed by this
711     * map frame; null, if no such dialog exists
712     *
713     */
714    public <T> T getToggleDialog(Class<T> type) {
715        return dialogsPanel.getToggleDialog(type);
716    }
717
718    /**
719     * Shows or hides the side dialog panel
720     * @param visible The new visibility
721     */
722    public void setDialogsPanelVisible(boolean visible) {
723        rememberToggleDialogWidth();
724        dialogsPanel.setVisible(visible);
725        splitPane.setDividerLocation(visible ? splitPane.getWidth() - TOGGLE_DIALOGS_WIDTH.get() : 0);
726        splitPane.setDividerSize(visible ? 5 : 0);
727    }
728
729    /**
730     * Remember the current width of the (possibly resized) toggle dialog area
731     */
732    public void rememberToggleDialogWidth() {
733        if (dialogsPanel.isVisible()) {
734            TOGGLE_DIALOGS_WIDTH.put(splitPane.getWidth() - splitPane.getDividerLocation());
735        }
736    }
737
738    /**
739     * Remove panel from top of MapView by class
740     * @param type type of panel
741     */
742    public void removeTopPanel(Class<?> type) {
743        int n = leftPanel.getComponentCount();
744        for (int i = 0; i < n; i++) {
745            Component c = leftPanel.getComponent(i);
746            if (type.isInstance(c)) {
747                leftPanel.remove(i);
748                leftPanel.doLayout();
749                return;
750            }
751        }
752    }
753
754    /**
755     * Find panel on top of MapView by class
756     * @param <T> type
757     * @param type type of panel
758     * @return found panel
759     */
760    public <T> T getTopPanel(Class<T> type) {
761        int n = leftPanel.getComponentCount();
762        for (int i = 0; i < n; i++) {
763            Component c = leftPanel.getComponent(i);
764            if (type.isInstance(c))
765                return type.cast(c);
766        }
767        return null;
768    }
769
770    /**
771     * Add component {@code c} on top of MapView
772     * @param c component
773     */
774    public void addTopPanel(Component c) {
775        leftPanel.add(c, GBC.eol().fill(GBC.HORIZONTAL), leftPanel.getComponentCount()-1);
776        leftPanel.doLayout();
777        c.doLayout();
778    }
779
780    /**
781     * Interface to notify listeners of the change of the mapMode.
782     * @since 10600 (functional interface)
783     */
784    @FunctionalInterface
785    public interface MapModeChangeListener {
786        /**
787         * Trigerred when map mode changes.
788         * @param oldMapMode old map mode
789         * @param newMapMode new map mode
790         */
791        void mapModeChange(MapMode oldMapMode, MapMode newMapMode);
792    }
793
794    /**
795     * the mapMode listeners
796     */
797    private static final CopyOnWriteArrayList<MapModeChangeListener> mapModeChangeListeners = new CopyOnWriteArrayList<>();
798
799    private transient PreferenceChangedListener sidetoolbarPreferencesChangedListener;
800    /**
801     * Adds a mapMode change listener
802     *
803     * @param listener the listener. Ignored if null or already registered.
804     */
805    public static void addMapModeChangeListener(MapModeChangeListener listener) {
806        if (listener != null) {
807            mapModeChangeListeners.addIfAbsent(listener);
808        }
809    }
810
811    /**
812     * Removes a mapMode change listener
813     *
814     * @param listener the listener. Ignored if null or already registered.
815     */
816    public static void removeMapModeChangeListener(MapModeChangeListener listener) {
817        mapModeChangeListeners.remove(listener);
818    }
819
820    protected static void fireMapModeChanged(MapMode oldMapMode, MapMode newMapMode) {
821        for (MapModeChangeListener l : mapModeChangeListeners) {
822            l.mapModeChange(oldMapMode, newMapMode);
823        }
824    }
825
826    @Override
827    public void activeOrEditLayerChanged(ActiveLayerChangeEvent e) {
828        boolean modeChanged = false;
829        Layer newLayer = e.getSource().getActiveLayer();
830        if (mapMode == null || !mapMode.layerIsSupported(newLayer)) {
831            MapMode newMapMode = getLastMapMode(newLayer);
832            modeChanged = newMapMode != mapMode;
833            if (newMapMode != null) {
834                // it would be nice to select first supported mode when layer is first selected,
835                // but it don't work well with for example editgpx layer
836                selectMapMode(newMapMode, newLayer);
837            } else if (mapMode != null) {
838                mapMode.exitMode(); // if new mode is null - simply exit from previous mode
839                mapMode = null;
840            }
841        }
842        // if this is really a change (and not the first active layer)
843        if (e.getPreviousActiveLayer() != null && !modeChanged && mapMode != null) {
844            // Let mapmodes know about new active layer
845            mapMode.exitMode();
846            mapMode.enterMode();
847        }
848
849        // After all listeners notice new layer, some buttons will be disabled/enabled
850        // and possibly need to be hidden/shown.
851        validateToolBarsVisibility();
852    }
853
854    private MapMode getLastMapMode(Layer newLayer) {
855        MapMode mode = lastMapMode.get(newLayer);
856        if (mode == null) {
857            // if no action is selected - try to select default action
858            Action defaultMode = getDefaultButtonAction();
859            if (defaultMode instanceof MapMode && ((MapMode) defaultMode).layerIsSupported(newLayer)) {
860                mode = (MapMode) defaultMode;
861            }
862        }
863        return mode;
864    }
865
866    @Override
867    public void layerAdded(LayerAddEvent e) {
868        // ignored
869    }
870
871    @Override
872    public void layerRemoving(LayerRemoveEvent e) {
873        lastMapMode.remove(e.getRemovedLayer());
874    }
875
876    @Override
877    public void layerOrderChanged(LayerOrderChangeEvent e) {
878        // ignored
879    }
880
881}