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