001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.dialogs;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.awt.Color;
007import java.awt.Component;
008import java.awt.Dimension;
009import java.awt.Font;
010import java.awt.Point;
011import java.awt.Rectangle;
012import java.awt.event.ActionEvent;
013import java.awt.event.InputEvent;
014import java.awt.event.KeyEvent;
015import java.awt.event.MouseEvent;
016import java.beans.PropertyChangeEvent;
017import java.beans.PropertyChangeListener;
018import java.lang.ref.WeakReference;
019import java.util.ArrayList;
020import java.util.Arrays;
021import java.util.Collections;
022import java.util.List;
023import java.util.concurrent.CopyOnWriteArrayList;
024
025import javax.swing.AbstractAction;
026import javax.swing.DefaultCellEditor;
027import javax.swing.DefaultListSelectionModel;
028import javax.swing.ImageIcon;
029import javax.swing.JCheckBox;
030import javax.swing.JComponent;
031import javax.swing.JLabel;
032import javax.swing.JMenuItem;
033import javax.swing.JPopupMenu;
034import javax.swing.JSlider;
035import javax.swing.JTable;
036import javax.swing.JViewport;
037import javax.swing.KeyStroke;
038import javax.swing.ListSelectionModel;
039import javax.swing.UIManager;
040import javax.swing.event.ChangeEvent;
041import javax.swing.event.ChangeListener;
042import javax.swing.event.ListDataEvent;
043import javax.swing.event.ListSelectionEvent;
044import javax.swing.event.ListSelectionListener;
045import javax.swing.event.TableModelEvent;
046import javax.swing.event.TableModelListener;
047import javax.swing.table.AbstractTableModel;
048import javax.swing.table.DefaultTableCellRenderer;
049import javax.swing.table.TableCellRenderer;
050import javax.swing.table.TableModel;
051
052import org.openstreetmap.josm.Main;
053import org.openstreetmap.josm.actions.MergeLayerAction;
054import org.openstreetmap.josm.gui.MapFrame;
055import org.openstreetmap.josm.gui.MapView;
056import org.openstreetmap.josm.gui.SideButton;
057import org.openstreetmap.josm.gui.help.HelpUtil;
058import org.openstreetmap.josm.gui.layer.JumpToMarkerActions;
059import org.openstreetmap.josm.gui.layer.Layer;
060import org.openstreetmap.josm.gui.layer.Layer.LayerAction;
061import org.openstreetmap.josm.gui.layer.OsmDataLayer;
062import org.openstreetmap.josm.gui.util.GuiHelper;
063import org.openstreetmap.josm.gui.widgets.DisableShortcutsOnFocusGainedTextField;
064import org.openstreetmap.josm.gui.widgets.JosmTextField;
065import org.openstreetmap.josm.gui.widgets.PopupMenuLauncher;
066import org.openstreetmap.josm.tools.CheckParameterUtil;
067import org.openstreetmap.josm.tools.ImageProvider;
068import org.openstreetmap.josm.tools.InputMapUtils;
069import org.openstreetmap.josm.tools.MultikeyActionsHandler;
070import org.openstreetmap.josm.tools.MultikeyShortcutAction;
071import org.openstreetmap.josm.tools.MultikeyShortcutAction.MultikeyInfo;
072import org.openstreetmap.josm.tools.Shortcut;
073
074/**
075 * This is a toggle dialog which displays the list of layers. Actions allow to
076 * change the ordering of the layers, to hide/show layers, to activate layers,
077 * and to delete layers.
078 * @since 17
079 */
080public class LayerListDialog extends ToggleDialog {
081    /** the unique instance of the dialog */
082    private static LayerListDialog instance;
083
084    /**
085     * Creates the instance of the dialog. It's connected to the map frame <code>mapFrame</code>
086     *
087     * @param mapFrame the map frame
088     */
089    public static void createInstance(MapFrame mapFrame) {
090        if (instance != null)
091            throw new IllegalStateException("Dialog was already created");
092        instance = new LayerListDialog(mapFrame);
093    }
094
095    /**
096     * Replies the instance of the dialog
097     *
098     * @return the instance of the dialog
099     * @throws IllegalStateException thrown, if the dialog is not created yet
100     * @see #createInstance(MapFrame)
101     */
102    public static LayerListDialog getInstance() throws IllegalStateException {
103        if (instance == null)
104            throw new IllegalStateException("Dialog not created yet. Invoke createInstance() first");
105        return instance;
106    }
107
108    /** the model for the layer list */
109    private LayerListModel model;
110
111    /** the list of layers (technically its a JTable, but appears like a list) */
112    private LayerList layerList;
113
114    private SideButton opacityButton;
115
116    ActivateLayerAction activateLayerAction;
117    ShowHideLayerAction showHideLayerAction;
118
119    //TODO This duplicates ShowHide actions functionality
120    /** stores which layer index to toggle and executes the ShowHide action if the layer is present */
121    private final class ToggleLayerIndexVisibility extends AbstractAction {
122        int layerIndex = -1;
123        public ToggleLayerIndexVisibility(int layerIndex) {
124            this.layerIndex = layerIndex;
125        }
126        @Override
127        public void actionPerformed(ActionEvent e) {
128            final Layer l = model.getLayer(model.getRowCount() - layerIndex - 1);
129            if (l != null) {
130                l.toggleVisible();
131            }
132        }
133    }
134
135    private final Shortcut[] visibilityToggleShortcuts = new Shortcut[10];
136    private final ToggleLayerIndexVisibility[] visibilityToggleActions = new ToggleLayerIndexVisibility[10];
137
138    /**
139     * registers (shortcut to toggle right hand side toggle dialogs)+(number keys) shortcuts
140     * to toggle the visibility of the first ten layers.
141     */
142    private final void createVisibilityToggleShortcuts() {
143        final int[] k = { KeyEvent.VK_1, KeyEvent.VK_2, KeyEvent.VK_3, KeyEvent.VK_4,
144                KeyEvent.VK_5, KeyEvent.VK_6, KeyEvent.VK_7, KeyEvent.VK_8,
145                KeyEvent.VK_9, KeyEvent.VK_0 };
146
147        for(int i=0; i < 10; i++) {
148            visibilityToggleShortcuts[i] = Shortcut.registerShortcut("subwindow:layers:toggleLayer" + (i+1),
149                    tr("Toggle visibility of layer: {0}", (i+1)), k[i], Shortcut.ALT);
150            visibilityToggleActions[i] = new ToggleLayerIndexVisibility(i);
151            Main.registerActionShortcut(visibilityToggleActions[i], visibilityToggleShortcuts[i]);
152        }
153    }
154
155    /**
156     * Creates a layer list and attach it to the given mapView.
157     */
158    protected LayerListDialog(MapFrame mapFrame) {
159        super(tr("Layers"), "layerlist", tr("Open a list of all loaded layers."),
160                Shortcut.registerShortcut("subwindow:layers", tr("Toggle: {0}", tr("Layers")), KeyEvent.VK_L,
161                        Shortcut.ALT_SHIFT), 100, true);
162
163        // create the models
164        //
165        DefaultListSelectionModel selectionModel = new DefaultListSelectionModel();
166        selectionModel.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
167        model = new LayerListModel(selectionModel);
168
169        // create the list control
170        //
171        layerList = new LayerList(model);
172        layerList.setSelectionModel(selectionModel);
173        layerList.addMouseListener(new PopupMenuHandler());
174        layerList.setBackground(UIManager.getColor("Button.background"));
175        layerList.putClientProperty("terminateEditOnFocusLost", true);
176        layerList.putClientProperty("JTable.autoStartsEdit", false);
177        layerList.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
178        layerList.setTableHeader(null);
179        layerList.setShowGrid(false);
180        layerList.setIntercellSpacing(new Dimension(0, 0));
181        layerList.getColumnModel().getColumn(0).setCellRenderer(new ActiveLayerCellRenderer());
182        layerList.getColumnModel().getColumn(0).setCellEditor(new DefaultCellEditor(new ActiveLayerCheckBox()));
183        layerList.getColumnModel().getColumn(0).setMaxWidth(12);
184        layerList.getColumnModel().getColumn(0).setPreferredWidth(12);
185        layerList.getColumnModel().getColumn(0).setResizable(false);
186        layerList.getColumnModel().getColumn(1).setCellRenderer(new LayerVisibleCellRenderer());
187        layerList.getColumnModel().getColumn(1).setCellEditor(new LayerVisibleCellEditor(new LayerVisibleCheckBox()));
188        layerList.getColumnModel().getColumn(1).setMaxWidth(16);
189        layerList.getColumnModel().getColumn(1).setPreferredWidth(16);
190        layerList.getColumnModel().getColumn(1).setResizable(false);
191        layerList.getColumnModel().getColumn(2).setCellRenderer(new LayerNameCellRenderer());
192        layerList.getColumnModel().getColumn(2).setCellEditor(new LayerNameCellEditor(new DisableShortcutsOnFocusGainedTextField()));
193        // Disable some default JTable shortcuts to use JOSM ones (see #5678, #10458)
194        for (KeyStroke ks : new KeyStroke[] {
195                KeyStroke.getKeyStroke(KeyEvent.VK_C, GuiHelper.getMenuShortcutKeyMaskEx()),
196                KeyStroke.getKeyStroke(KeyEvent.VK_V, GuiHelper.getMenuShortcutKeyMaskEx()),
197                KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, InputEvent.SHIFT_DOWN_MASK),
198                KeyStroke.getKeyStroke(KeyEvent.VK_UP, InputEvent.SHIFT_DOWN_MASK),
199                KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, InputEvent.SHIFT_DOWN_MASK),
200                KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, InputEvent.SHIFT_DOWN_MASK),
201                KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, InputEvent.CTRL_DOWN_MASK),
202                KeyStroke.getKeyStroke(KeyEvent.VK_UP, InputEvent.CTRL_DOWN_MASK),
203                KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, InputEvent.CTRL_DOWN_MASK),
204                KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, InputEvent.CTRL_DOWN_MASK),
205                KeyStroke.getKeyStroke(KeyEvent.VK_PAGE_UP, 0),
206                KeyStroke.getKeyStroke(KeyEvent.VK_PAGE_DOWN, 0),
207                KeyStroke.getKeyStroke(KeyEvent.VK_TAB, 0),
208                KeyStroke.getKeyStroke(KeyEvent.VK_F8, 0),
209        })
210        {
211            layerList.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(ks, new Object());
212        }
213
214        // init the model
215        //
216        final MapView mapView = mapFrame.mapView;
217        model.populate();
218        model.setSelectedLayer(mapView.getActiveLayer());
219        model.addLayerListModelListener(
220                new LayerListModelListener() {
221                    @Override
222                    public void makeVisible(int row, Layer layer) {
223                        layerList.scrollToVisible(row, 0);
224                        layerList.repaint();
225                    }
226                    @Override
227                    public void refresh() {
228                        layerList.repaint();
229                    }
230                }
231                );
232
233        // -- move up action
234        MoveUpAction moveUpAction = new MoveUpAction();
235        adaptTo(moveUpAction, model);
236        adaptTo(moveUpAction,selectionModel);
237
238        // -- move down action
239        MoveDownAction moveDownAction = new MoveDownAction();
240        adaptTo(moveDownAction, model);
241        adaptTo(moveDownAction,selectionModel);
242
243        // -- activate action
244        activateLayerAction = new ActivateLayerAction();
245        activateLayerAction.updateEnabledState();
246        MultikeyActionsHandler.getInstance().addAction(activateLayerAction);
247        adaptTo(activateLayerAction, selectionModel);
248
249        JumpToMarkerActions.initialize();
250
251        // -- show hide action
252        showHideLayerAction = new ShowHideLayerAction();
253        MultikeyActionsHandler.getInstance().addAction(showHideLayerAction);
254        adaptTo(showHideLayerAction, selectionModel);
255
256        // -- layer opacity action
257        LayerOpacityAction layerOpacityAction = new LayerOpacityAction();
258        adaptTo(layerOpacityAction, selectionModel);
259        opacityButton = new SideButton(layerOpacityAction, false);
260
261        // -- merge layer action
262        MergeAction mergeLayerAction = new MergeAction();
263        adaptTo(mergeLayerAction, model);
264        adaptTo(mergeLayerAction,selectionModel);
265
266        // -- duplicate layer action
267        DuplicateAction duplicateLayerAction = new DuplicateAction();
268        adaptTo(duplicateLayerAction, model);
269        adaptTo(duplicateLayerAction, selectionModel);
270
271        // -- delete layer action
272        DeleteLayerAction deleteLayerAction = new DeleteLayerAction();
273        layerList.getActionMap().put("deleteLayer", deleteLayerAction);
274        adaptTo(deleteLayerAction, selectionModel);
275        getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(
276                KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0),"delete"
277                );
278        getActionMap().put("delete", deleteLayerAction);
279
280        // Activate layer on Enter key press
281        InputMapUtils.addEnterAction(layerList, new AbstractAction() {
282            @Override
283            public void actionPerformed(ActionEvent e) {
284                activateLayerAction.actionPerformed(null);
285                layerList.requestFocus();
286            }
287        });
288
289        // Show/Activate layer on Enter key press
290        InputMapUtils.addSpacebarAction(layerList, showHideLayerAction);
291
292        createLayout(layerList, true, Arrays.asList(new SideButton[] {
293                new SideButton(moveUpAction, false),
294                new SideButton(moveDownAction, false),
295                new SideButton(activateLayerAction, false),
296                new SideButton(showHideLayerAction, false),
297                opacityButton,
298                new SideButton(mergeLayerAction, false),
299                new SideButton(duplicateLayerAction, false),
300                new SideButton(deleteLayerAction, false)
301        }));
302
303        createVisibilityToggleShortcuts();
304    }
305
306    @Override
307    public void showNotify() {
308        MapView.addLayerChangeListener(activateLayerAction);
309        MapView.addLayerChangeListener(model);
310        model.populate();
311    }
312
313    @Override
314    public void hideNotify() {
315        MapView.removeLayerChangeListener(model);
316        MapView.removeLayerChangeListener(activateLayerAction);
317    }
318
319    /**
320     * Returns the layer list model.
321     * @return the layer list model
322     */
323    public LayerListModel getModel() {
324        return model;
325    }
326
327    protected interface IEnabledStateUpdating {
328        void updateEnabledState();
329    }
330
331    /**
332     * Wires <code>listener</code> to <code>listSelectionModel</code> in such a way, that
333     * <code>listener</code> receives a {@link IEnabledStateUpdating#updateEnabledState()}
334     * on every {@link ListSelectionEvent}.
335     *
336     * @param listener  the listener
337     * @param listSelectionModel  the source emitting {@link ListSelectionEvent}s
338     */
339    protected void adaptTo(final IEnabledStateUpdating listener, ListSelectionModel listSelectionModel) {
340        listSelectionModel.addListSelectionListener(
341                new ListSelectionListener() {
342                    @Override
343                    public void valueChanged(ListSelectionEvent e) {
344                        listener.updateEnabledState();
345                    }
346                }
347                );
348    }
349
350    /**
351     * Wires <code>listener</code> to <code>listModel</code> in such a way, that
352     * <code>listener</code> receives a {@link IEnabledStateUpdating#updateEnabledState()}
353     * on every {@link ListDataEvent}.
354     *
355     * @param listener the listener
356     * @param listModel the source emitting {@link ListDataEvent}s
357     */
358    protected void adaptTo(final IEnabledStateUpdating listener, LayerListModel listModel) {
359        listModel.addTableModelListener(
360                new TableModelListener() {
361
362                    @Override
363                    public void tableChanged(TableModelEvent e) {
364                        listener.updateEnabledState();
365                    }
366                }
367                );
368    }
369
370    @Override
371    public void destroy() {
372        for(int i=0; i < 10; i++) {
373            Main.unregisterActionShortcut(visibilityToggleActions[i], visibilityToggleShortcuts[i]);
374        }
375        MultikeyActionsHandler.getInstance().removeAction(activateLayerAction);
376        MultikeyActionsHandler.getInstance().removeAction(showHideLayerAction);
377        JumpToMarkerActions.unregisterActions();
378        super.destroy();
379        instance = null;
380    }
381
382    /**
383     * The action to delete the currently selected layer
384     */
385    public final class DeleteLayerAction extends AbstractAction implements IEnabledStateUpdating, LayerAction {
386
387        /**
388         * Creates a {@link DeleteLayerAction} which will delete the currently
389         * selected layers in the layer dialog.
390         */
391        public DeleteLayerAction() {
392            putValue(SMALL_ICON,ImageProvider.get("dialogs", "delete"));
393            putValue(SHORT_DESCRIPTION, tr("Delete the selected layers."));
394            putValue(NAME, tr("Delete"));
395            putValue("help", HelpUtil.ht("/Dialog/LayerList#DeleteLayer"));
396            updateEnabledState();
397        }
398
399        @Override
400        public void actionPerformed(ActionEvent e) {
401            List<Layer> selectedLayers = getModel().getSelectedLayers();
402            if (selectedLayers.isEmpty())
403                return;
404            if (!Main.saveUnsavedModifications(selectedLayers, false))
405                return;
406            for (Layer l: selectedLayers) {
407                Main.main.removeLayer(l);
408            }
409        }
410
411        @Override
412        public void updateEnabledState() {
413            setEnabled(! getModel().getSelectedLayers().isEmpty());
414        }
415
416        @Override
417        public Component createMenuComponent() {
418            return new JMenuItem(this);
419        }
420
421        @Override
422        public boolean supportLayers(List<Layer> layers) {
423            return true;
424        }
425
426        @Override
427        public boolean equals(Object obj) {
428            return obj instanceof DeleteLayerAction;
429        }
430
431        @Override
432        public int hashCode() {
433            return getClass().hashCode();
434        }
435    }
436
437    /**
438     * Action which will toggle the visibility of the currently selected layers.
439     */
440    public final class ShowHideLayerAction extends AbstractAction implements IEnabledStateUpdating, LayerAction, MultikeyShortcutAction {
441
442        private WeakReference<Layer> lastLayer;
443        private Shortcut multikeyShortcut;
444
445        /**
446         * Creates a {@link ShowHideLayerAction} which will toggle the visibility of
447         * the currently selected layers
448         */
449        public ShowHideLayerAction() {
450            putValue(NAME, tr("Show/hide"));
451            putValue(SMALL_ICON, ImageProvider.get("dialogs", "showhide"));
452            putValue(SHORT_DESCRIPTION, tr("Toggle visible state of the selected layer."));
453            putValue("help", HelpUtil.ht("/Dialog/LayerList#ShowHideLayer"));
454            multikeyShortcut = Shortcut.registerShortcut("core_multikey:showHideLayer", tr("Multikey: {0}",
455                    tr("Show/hide layer")), KeyEvent.VK_S, Shortcut.SHIFT);
456            multikeyShortcut.setAccelerator(this);
457            updateEnabledState();
458        }
459
460        @Override
461        public Shortcut getMultikeyShortcut() {
462            return multikeyShortcut;
463        }
464
465        @Override
466        public void actionPerformed(ActionEvent e) {
467            for(Layer l : model.getSelectedLayers()) {
468                l.toggleVisible();
469            }
470        }
471
472        @Override
473        public void executeMultikeyAction(int index, boolean repeat) {
474            Layer l = LayerListDialog.getLayerForIndex(index);
475            if (l != null) {
476                l.toggleVisible();
477                lastLayer = new WeakReference<>(l);
478            } else if (repeat && lastLayer != null) {
479                l = lastLayer.get();
480                if (LayerListDialog.isLayerValid(l)) {
481                    l.toggleVisible();
482                }
483            }
484        }
485
486        @Override
487        public void updateEnabledState() {
488            setEnabled(!model.getSelectedLayers().isEmpty());
489        }
490
491        @Override
492        public Component createMenuComponent() {
493            return new JMenuItem(this);
494        }
495
496        @Override
497        public boolean supportLayers(List<Layer> layers) {
498            return true;
499        }
500
501        @Override
502        public boolean equals(Object obj) {
503            return obj instanceof ShowHideLayerAction;
504        }
505
506        @Override
507        public int hashCode() {
508            return getClass().hashCode();
509        }
510
511        @Override
512        public List<MultikeyInfo> getMultikeyCombinations() {
513            return LayerListDialog.getLayerInfoByClass(Layer.class);
514        }
515
516        @Override
517        public MultikeyInfo getLastMultikeyAction() {
518            if (lastLayer != null)
519                return LayerListDialog.getLayerInfo(lastLayer.get());
520            return null;
521        }
522    }
523
524    /**
525     * Action which allows to change the opacity of one or more layers.
526     */
527    public final class LayerOpacityAction extends AbstractAction implements IEnabledStateUpdating, LayerAction {
528        private Layer layer;
529        private JPopupMenu popup;
530        private JSlider slider = new JSlider(JSlider.VERTICAL);
531
532        /**
533         * Creates a {@link LayerOpacityAction} which allows to change the
534         * opacity of one or more layers.
535         *
536         * @param layer  the layer. Must not be null.
537         * @exception IllegalArgumentException thrown, if layer is null
538         */
539        public LayerOpacityAction(Layer layer) throws IllegalArgumentException {
540            this();
541            putValue(NAME, tr("Opacity"));
542            CheckParameterUtil.ensureParameterNotNull(layer, "layer");
543            this.layer = layer;
544            updateEnabledState();
545        }
546
547        /**
548         * Creates a {@link ShowHideLayerAction} which will toggle the visibility of
549         * the currently selected layers
550         *
551         */
552        public LayerOpacityAction() {
553            putValue(NAME, tr("Opacity"));
554            putValue(SHORT_DESCRIPTION, tr("Adjust opacity of the layer."));
555            putValue(SMALL_ICON, ImageProvider.get("dialogs/layerlist", "transparency"));
556            updateEnabledState();
557
558            popup = new JPopupMenu();
559            slider.addChangeListener(new ChangeListener() {
560                @Override
561                public void stateChanged(ChangeEvent e) {
562                    setOpacity((double)slider.getValue()/100);
563                }
564            });
565            popup.add(slider);
566        }
567
568        private void setOpacity(double value) {
569            if (!isEnabled()) return;
570            if (layer != null) {
571                layer.setOpacity(value);
572            } else {
573                for(Layer layer: model.getSelectedLayers()) {
574                    layer.setOpacity(value);
575                }
576            }
577        }
578
579        private double getOpacity() {
580            if (layer != null)
581                return layer.getOpacity();
582            else {
583                double opacity = 0;
584                List<Layer> layers = model.getSelectedLayers();
585                for(Layer layer: layers) {
586                    opacity += layer.getOpacity();
587                }
588                return opacity / layers.size();
589            }
590        }
591
592        @Override
593        public void actionPerformed(ActionEvent e) {
594            slider.setValue((int)Math.round(getOpacity()*100));
595            if (e.getSource() == opacityButton) {
596                popup.show(opacityButton, 0, opacityButton.getHeight());
597            } else {
598                // Action can be trigger either by opacity button or by popup menu (in case toggle buttons are hidden).
599                // In that case, show it in the middle of screen (because opacityButton is not visible)
600                popup.show(Main.parent, Main.parent.getWidth() / 2, (Main.parent.getHeight() - popup.getHeight()) / 2);
601            }
602        }
603
604        @Override
605        public void updateEnabledState() {
606            if (layer == null) {
607                setEnabled(! getModel().getSelectedLayers().isEmpty());
608            } else {
609                setEnabled(true);
610            }
611        }
612
613        @Override
614        public Component createMenuComponent() {
615            return new JMenuItem(this);
616        }
617
618        @Override
619        public boolean supportLayers(List<Layer> layers) {
620            return true;
621        }
622
623        @Override
624        public boolean equals(Object obj) {
625            return obj instanceof LayerOpacityAction;
626        }
627
628        @Override
629        public int hashCode() {
630            return getClass().hashCode();
631        }
632    }
633
634    /**
635     * The action to activate the currently selected layer
636     */
637
638    public final class ActivateLayerAction extends AbstractAction implements IEnabledStateUpdating, MapView.LayerChangeListener, MultikeyShortcutAction{
639        private  Layer layer;
640        private Shortcut multikeyShortcut;
641
642        /**
643         * Constructs a new {@code ActivateLayerAction}.
644         * @param layer the layer
645         */
646        public ActivateLayerAction(Layer layer) {
647            this();
648            CheckParameterUtil.ensureParameterNotNull(layer, "layer");
649            this.layer = layer;
650            putValue(NAME, tr("Activate"));
651            updateEnabledState();
652        }
653
654        /**
655         * Constructs a new {@code ActivateLayerAction}.
656         */
657        public ActivateLayerAction() {
658            putValue(NAME, tr("Activate"));
659            putValue(SMALL_ICON, ImageProvider.get("dialogs", "activate"));
660            putValue(SHORT_DESCRIPTION, tr("Activate the selected layer"));
661            multikeyShortcut = Shortcut.registerShortcut("core_multikey:activateLayer", tr("Multikey: {0}",
662                    tr("Activate layer")), KeyEvent.VK_A, Shortcut.SHIFT);
663            multikeyShortcut.setAccelerator(this);
664            putValue("help", HelpUtil.ht("/Dialog/LayerList#ActivateLayer"));
665        }
666
667        @Override
668        public Shortcut getMultikeyShortcut() {
669            return multikeyShortcut;
670        }
671
672        @Override
673        public void actionPerformed(ActionEvent e) {
674            Layer toActivate;
675            if (layer != null) {
676                toActivate = layer;
677            } else {
678                toActivate = model.getSelectedLayers().get(0);
679            }
680            execute(toActivate);
681        }
682
683        private void execute(Layer layer) {
684            // model is  going to be updated via LayerChangeListener and PropertyChangeEvents
685            Main.map.mapView.setActiveLayer(layer);
686            layer.setVisible(true);
687        }
688
689        protected boolean isActiveLayer(Layer layer) {
690            if (!Main.isDisplayingMapView()) return false;
691            return Main.map.mapView.getActiveLayer() == layer;
692        }
693
694        @Override
695        public void updateEnabledState() {
696            GuiHelper.runInEDTAndWait(new Runnable() {
697                @Override
698                public void run() {
699                    if (layer == null) {
700                        if (getModel().getSelectedLayers().size() != 1) {
701                            setEnabled(false);
702                            return;
703                        }
704                        Layer selectedLayer = getModel().getSelectedLayers().get(0);
705                        setEnabled(!isActiveLayer(selectedLayer));
706                    } else {
707                        setEnabled(!isActiveLayer(layer));
708                    }
709                }
710            });
711        }
712
713        @Override
714        public void activeLayerChange(Layer oldLayer, Layer newLayer) {
715            updateEnabledState();
716        }
717
718        @Override
719        public void layerAdded(Layer newLayer) {
720            updateEnabledState();
721        }
722
723        @Override
724        public void layerRemoved(Layer oldLayer) {
725            updateEnabledState();
726        }
727
728        @Override
729        public void executeMultikeyAction(int index, boolean repeat) {
730            Layer l = LayerListDialog.getLayerForIndex(index);
731            if (l != null) {
732                execute(l);
733            }
734        }
735
736        @Override
737        public List<MultikeyInfo> getMultikeyCombinations() {
738            return LayerListDialog.getLayerInfoByClass(Layer.class);
739        }
740
741        @Override
742        public MultikeyInfo getLastMultikeyAction() {
743            return null; // Repeating action doesn't make much sense for activating
744        }
745    }
746
747    /**
748     * The action to merge the currently selected layer into another layer.
749     */
750    public final class MergeAction extends AbstractAction implements IEnabledStateUpdating {
751        private  Layer layer;
752
753        /**
754         * Constructs a new {@code MergeAction}.
755         * @param layer the layer
756         * @throws IllegalArgumentException if {@code layer} is null
757         */
758        public MergeAction(Layer layer) throws IllegalArgumentException {
759            this();
760            CheckParameterUtil.ensureParameterNotNull(layer, "layer");
761            this.layer = layer;
762            putValue(NAME, tr("Merge"));
763            updateEnabledState();
764        }
765
766        /**
767         * Constructs a new {@code MergeAction}.
768         */
769        public MergeAction() {
770            putValue(NAME, tr("Merge"));
771            putValue(SMALL_ICON, ImageProvider.get("dialogs", "mergedown"));
772            putValue(SHORT_DESCRIPTION, tr("Merge this layer into another layer"));
773            putValue("help", HelpUtil.ht("/Dialog/LayerList#MergeLayer"));
774            updateEnabledState();
775        }
776
777        @Override
778        public void actionPerformed(ActionEvent e) {
779            if (layer != null) {
780                Main.main.menu.merge.merge(layer);
781            } else {
782                if (getModel().getSelectedLayers().size() == 1) {
783                    Layer selectedLayer = getModel().getSelectedLayers().get(0);
784                    Main.main.menu.merge.merge(selectedLayer);
785                } else {
786                    Main.main.menu.merge.merge(getModel().getSelectedLayers());
787                }
788            }
789        }
790
791        protected boolean isActiveLayer(Layer layer) {
792            if (!Main.isDisplayingMapView()) return false;
793            return Main.map.mapView.getActiveLayer() == layer;
794        }
795
796        @Override
797        public void updateEnabledState() {
798            if (layer == null) {
799                if (getModel().getSelectedLayers().isEmpty()) {
800                    setEnabled(false);
801                } else  if (getModel().getSelectedLayers().size() > 1) {
802                    Layer firstLayer = getModel().getSelectedLayers().get(0);
803                    for (Layer l: getModel().getSelectedLayers()) {
804                        if (l != firstLayer && (!l.isMergable(firstLayer) || !firstLayer.isMergable(l))) {
805                            setEnabled(false);
806                            return;
807                        }
808                    }
809                    setEnabled(true);
810                } else {
811                    Layer selectedLayer = getModel().getSelectedLayers().get(0);
812                    List<Layer> targets = getModel().getPossibleMergeTargets(selectedLayer);
813                    setEnabled(!targets.isEmpty());
814                }
815            } else {
816                List<Layer> targets = getModel().getPossibleMergeTargets(layer);
817                setEnabled(!targets.isEmpty());
818            }
819        }
820    }
821
822    /**
823     * The action to merge the currently selected layer into another layer.
824     */
825    public final class DuplicateAction extends AbstractAction implements IEnabledStateUpdating {
826        private Layer layer;
827
828        /**
829         * Constructs a new {@code DuplicateAction}.
830         * @param layer the layer
831         * @throws IllegalArgumentException if {@code layer} is null
832         */
833        public DuplicateAction(Layer layer) throws IllegalArgumentException {
834            this();
835            CheckParameterUtil.ensureParameterNotNull(layer, "layer");
836            this.layer = layer;
837            updateEnabledState();
838        }
839
840        /**
841         * Constructs a new {@code DuplicateAction}.
842         */
843        public DuplicateAction() {
844            putValue(NAME, tr("Duplicate"));
845            putValue(SMALL_ICON, ImageProvider.get("dialogs", "duplicatelayer"));
846            putValue(SHORT_DESCRIPTION, tr("Duplicate this layer"));
847            putValue("help", HelpUtil.ht("/Dialog/LayerList#DuplicateLayer"));
848            updateEnabledState();
849        }
850
851        private void duplicate(Layer layer) {
852            if (!Main.isDisplayingMapView())
853                return;
854
855            List<String> layerNames = new ArrayList<>();
856            for (Layer l: Main.map.mapView.getAllLayers()) {
857                layerNames.add(l.getName());
858            }
859            if (layer instanceof OsmDataLayer) {
860                OsmDataLayer oldLayer = (OsmDataLayer)layer;
861                // Translators: "Copy of {layer name}"
862                String newName = tr("Copy of {0}", oldLayer.getName());
863                int i = 2;
864                while (layerNames.contains(newName)) {
865                    // Translators: "Copy {number} of {layer name}"
866                    newName = tr("Copy {1} of {0}", oldLayer.getName(), i);
867                    i++;
868                }
869                Main.main.addLayer(new OsmDataLayer(oldLayer.data.clone(), newName, null));
870            }
871        }
872
873        @Override
874        public void actionPerformed(ActionEvent e) {
875            if (layer != null) {
876                duplicate(layer);
877            } else {
878                duplicate(getModel().getSelectedLayers().get(0));
879            }
880        }
881
882        protected boolean isActiveLayer(Layer layer) {
883            if (!Main.isDisplayingMapView())
884                return false;
885            return Main.map.mapView.getActiveLayer() == layer;
886        }
887
888        @Override
889        public void updateEnabledState() {
890            if (layer == null) {
891                if (getModel().getSelectedLayers().size() == 1) {
892                    setEnabled(getModel().getSelectedLayers().get(0) instanceof OsmDataLayer);
893                } else {
894                    setEnabled(false);
895                }
896            } else {
897                setEnabled(layer instanceof OsmDataLayer);
898            }
899        }
900    }
901
902    private static class ActiveLayerCheckBox extends JCheckBox {
903        public ActiveLayerCheckBox() {
904            setHorizontalAlignment(javax.swing.SwingConstants.CENTER);
905            ImageIcon blank = ImageProvider.get("dialogs/layerlist", "blank");
906            ImageIcon active = ImageProvider.get("dialogs/layerlist", "active");
907            setIcon(blank);
908            setSelectedIcon(active);
909            setRolloverIcon(blank);
910            setRolloverSelectedIcon(active);
911            setPressedIcon(ImageProvider.get("dialogs/layerlist", "active-pressed"));
912        }
913    }
914
915    private static class LayerVisibleCheckBox extends JCheckBox {
916        private final ImageIcon iconEye;
917        private final ImageIcon iconEyeTranslucent;
918        private boolean isTranslucent;
919        public LayerVisibleCheckBox() {
920            setHorizontalAlignment(javax.swing.SwingConstants.RIGHT);
921            iconEye = ImageProvider.get("dialogs/layerlist", "eye");
922            iconEyeTranslucent = ImageProvider.get("dialogs/layerlist", "eye-translucent");
923            setIcon(ImageProvider.get("dialogs/layerlist", "eye-off"));
924            setPressedIcon(ImageProvider.get("dialogs/layerlist", "eye-pressed"));
925            setSelectedIcon(iconEye);
926            isTranslucent = false;
927        }
928
929        public void setTranslucent(boolean isTranslucent) {
930            if (this.isTranslucent == isTranslucent) return;
931            if (isTranslucent) {
932                setSelectedIcon(iconEyeTranslucent);
933            } else {
934                setSelectedIcon(iconEye);
935            }
936            this.isTranslucent = isTranslucent;
937        }
938
939        public void updateStatus(Layer layer) {
940            boolean visible = layer.isVisible();
941            setSelected(visible);
942            setTranslucent(layer.getOpacity()<1.0);
943            setToolTipText(visible ? tr("layer is currently visible (click to hide layer)") : tr("layer is currently hidden (click to show layer)"));
944        }
945    }
946
947    private static class ActiveLayerCellRenderer implements TableCellRenderer {
948        final JCheckBox cb;
949        public ActiveLayerCellRenderer() {
950            cb = new ActiveLayerCheckBox();
951        }
952
953        @Override
954        public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
955            boolean active =  value != null && (Boolean) value;
956            cb.setSelected(active);
957            cb.setToolTipText(active ? tr("this layer is the active layer") : tr("this layer is not currently active (click to activate)"));
958            return cb;
959        }
960    }
961
962    private static class LayerVisibleCellRenderer implements TableCellRenderer {
963        final LayerVisibleCheckBox cb;
964        public LayerVisibleCellRenderer() {
965            this.cb = new LayerVisibleCheckBox();
966        }
967
968        @Override
969        public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
970            if (value != null) {
971                cb.updateStatus((Layer)value);
972            }
973            return cb;
974        }
975    }
976
977    private static class LayerVisibleCellEditor extends DefaultCellEditor {
978        final LayerVisibleCheckBox cb;
979        public LayerVisibleCellEditor(LayerVisibleCheckBox cb) {
980            super(cb);
981            this.cb = cb;
982        }
983
984        @Override
985        public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) {
986            cb.updateStatus((Layer)value);
987            return cb;
988        }
989    }
990
991    private class LayerNameCellRenderer extends DefaultTableCellRenderer {
992
993        protected boolean isActiveLayer(Layer layer) {
994            if (!Main.isDisplayingMapView()) return false;
995            return Main.map.mapView.getActiveLayer() == layer;
996        }
997
998        @Override
999        public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
1000            if (value == null)
1001                return this;
1002            Layer layer = (Layer)value;
1003            JLabel label = (JLabel)super.getTableCellRendererComponent(table,
1004                    layer.getName(), isSelected, hasFocus, row, column);
1005            if (isActiveLayer(layer)) {
1006                label.setFont(label.getFont().deriveFont(Font.BOLD));
1007            }
1008            if(Main.pref.getBoolean("dialog.layer.colorname", true)) {
1009                Color c = layer.getColor(false);
1010                if(c != null) {
1011                    Color oc = null;
1012                    for(Layer l : model.getLayers()) {
1013                        oc = l.getColor(false);
1014                        if(oc != null) {
1015                            if(oc.equals(c)) {
1016                                oc = null;
1017                            } else {
1018                                break;
1019                            }
1020                        }
1021                    }
1022                    /* not more than one color, don't use coloring */
1023                    if(oc == null) {
1024                        c = null;
1025                    }
1026                }
1027                if(c == null) {
1028                    c = Main.pref.getUIColor(isSelected ? "Table.selectionForeground" : "Table.foreground");
1029                }
1030                label.setForeground(c);
1031            }
1032            label.setIcon(layer.getIcon());
1033            label.setToolTipText(layer.getToolTipText());
1034            return label;
1035        }
1036    }
1037
1038    private static class LayerNameCellEditor extends DefaultCellEditor {
1039        public LayerNameCellEditor(DisableShortcutsOnFocusGainedTextField tf) {
1040            super(tf);
1041        }
1042
1043        @Override
1044        public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) {
1045            JosmTextField tf = (JosmTextField) super.getTableCellEditorComponent(table, value, isSelected, row, column);
1046            tf.setText(value == null ? "" : ((Layer) value).getName());
1047            return tf;
1048        }
1049    }
1050
1051    class PopupMenuHandler extends PopupMenuLauncher {
1052        @Override
1053        public void showMenu(MouseEvent evt) {
1054            Layer layer = getModel().getLayer(layerList.getSelectedRow());
1055            menu = new LayerListPopup(getModel().getSelectedLayers(), layer);
1056            super.showMenu(evt);
1057        }
1058    }
1059
1060    /**
1061     * The action to move up the currently selected entries in the list.
1062     */
1063    class MoveUpAction extends AbstractAction implements  IEnabledStateUpdating{
1064        public MoveUpAction() {
1065            putValue(NAME, tr("Move up"));
1066            putValue(SMALL_ICON, ImageProvider.get("dialogs", "up"));
1067            putValue(SHORT_DESCRIPTION, tr("Move the selected layer one row up."));
1068            updateEnabledState();
1069        }
1070
1071        @Override
1072        public void updateEnabledState() {
1073            setEnabled(model.canMoveUp());
1074        }
1075
1076        @Override
1077        public void actionPerformed(ActionEvent e) {
1078            model.moveUp();
1079        }
1080    }
1081
1082    /**
1083     * The action to move down the currently selected entries in the list.
1084     */
1085    class MoveDownAction extends AbstractAction implements IEnabledStateUpdating {
1086        public MoveDownAction() {
1087            putValue(NAME, tr("Move down"));
1088            putValue(SMALL_ICON, ImageProvider.get("dialogs", "down"));
1089            putValue(SHORT_DESCRIPTION, tr("Move the selected layer one row down."));
1090            updateEnabledState();
1091        }
1092
1093        @Override
1094        public void updateEnabledState() {
1095            setEnabled(model.canMoveDown());
1096        }
1097
1098        @Override
1099        public void actionPerformed(ActionEvent e) {
1100            model.moveDown();
1101        }
1102    }
1103
1104    /**
1105     * Observer interface to be implemented by views using {@link LayerListModel}.
1106     */
1107    public interface LayerListModelListener {
1108
1109        /**
1110         * Fired when a layer is made visible.
1111         * @param index the layer index
1112         * @param layer the layer
1113         */
1114        public void makeVisible(int index, Layer layer);
1115
1116
1117        /**
1118         * Fired when something has changed in the layer list model.
1119         */
1120        public void refresh();
1121    }
1122
1123    /**
1124     * The layer list model. The model manages a list of layers and provides methods for
1125     * moving layers up and down, for toggling their visibility, and for activating a layer.
1126     *
1127     * The model is a {@link TableModel} and it provides a {@link ListSelectionModel}. It expects
1128     * to be configured with a {@link DefaultListSelectionModel}. The selection model is used
1129     * to update the selection state of views depending on messages sent to the model.
1130     *
1131     * The model manages a list of {@link LayerListModelListener} which are mainly notified if
1132     * the model requires views to make a specific list entry visible.
1133     *
1134     * It also listens to {@link PropertyChangeEvent}s of every {@link Layer} it manages, in particular to
1135     * the properties {@link Layer#VISIBLE_PROP} and {@link Layer#NAME_PROP}.
1136     */
1137    public final class LayerListModel extends AbstractTableModel implements MapView.LayerChangeListener, PropertyChangeListener {
1138        /** manages list selection state*/
1139        private DefaultListSelectionModel selectionModel;
1140        private CopyOnWriteArrayList<LayerListModelListener> listeners;
1141
1142        /**
1143         * constructor
1144         *
1145         * @param selectionModel the list selection model
1146         */
1147        private LayerListModel(DefaultListSelectionModel selectionModel) {
1148            this.selectionModel = selectionModel;
1149            listeners = new CopyOnWriteArrayList<>();
1150        }
1151
1152        /**
1153         * Adds a listener to this model
1154         *
1155         * @param listener the listener
1156         */
1157        public void addLayerListModelListener(LayerListModelListener listener) {
1158            if (listener != null) {
1159                listeners.addIfAbsent(listener);
1160            }
1161        }
1162
1163        /**
1164         * removes a listener from  this model
1165         * @param listener the listener
1166         *
1167         */
1168        public void removeLayerListModelListener(LayerListModelListener listener) {
1169            listeners.remove(listener);
1170        }
1171
1172        /**
1173         * Fires a make visible event to listeners
1174         *
1175         * @param index the index of the row to make visible
1176         * @param layer the layer at this index
1177         * @see LayerListModelListener#makeVisible(int, Layer)
1178         */
1179        protected void fireMakeVisible(int index, Layer layer) {
1180            for (LayerListModelListener listener : listeners) {
1181                listener.makeVisible(index, layer);
1182            }
1183        }
1184
1185        /**
1186         * Fires a refresh event to listeners of this model
1187         *
1188         * @see LayerListModelListener#refresh()
1189         */
1190        protected void fireRefresh() {
1191            for (LayerListModelListener listener : listeners) {
1192                listener.refresh();
1193            }
1194        }
1195
1196        /**
1197         * Populates the model with the current layers managed by {@link MapView}.
1198         */
1199        public void populate() {
1200            for (Layer layer: getLayers()) {
1201                // make sure the model is registered exactly once
1202                layer.removePropertyChangeListener(this);
1203                layer.addPropertyChangeListener(this);
1204            }
1205            fireTableDataChanged();
1206        }
1207
1208        /**
1209         * Marks <code>layer</code> as selected layer. Ignored, if layer is null.
1210         *
1211         * @param layer the layer.
1212         */
1213        public void setSelectedLayer(Layer layer) {
1214            if (layer == null)
1215                return;
1216            int idx = getLayers().indexOf(layer);
1217            if (idx >= 0) {
1218                selectionModel.setSelectionInterval(idx, idx);
1219            }
1220            ensureSelectedIsVisible();
1221        }
1222
1223        /**
1224         * Replies the list of currently selected layers. Never null, but may be empty.
1225         *
1226         * @return the list of currently selected layers. Never null, but may be empty.
1227         */
1228        public List<Layer> getSelectedLayers() {
1229            List<Layer> selected = new ArrayList<>();
1230            for (int i=0; i<getLayers().size(); i++) {
1231                if (selectionModel.isSelectedIndex(i)) {
1232                    selected.add(getLayers().get(i));
1233                }
1234            }
1235            return selected;
1236        }
1237
1238        /**
1239         * Replies a the list of indices of the selected rows. Never null,
1240         * but may be empty.
1241         *
1242         * @return  the list of indices of the selected rows. Never null,
1243         * but may be empty.
1244         */
1245        public List<Integer> getSelectedRows() {
1246            List<Integer> selected = new ArrayList<>();
1247            for (int i=0; i<getLayers().size();i++) {
1248                if (selectionModel.isSelectedIndex(i)) {
1249                    selected.add(i);
1250                }
1251            }
1252            return selected;
1253        }
1254
1255        /**
1256         * Invoked if a layer managed by {@link MapView} is removed
1257         *
1258         * @param layer the layer which is removed
1259         */
1260        protected void onRemoveLayer(Layer layer) {
1261            if (layer == null)
1262                return;
1263            layer.removePropertyChangeListener(this);
1264            final int size = getRowCount();
1265            final List<Integer> rows = getSelectedRows();
1266            GuiHelper.runInEDTAndWait(new Runnable() {
1267                @Override
1268                public void run() {
1269                    if (rows.isEmpty() && size > 0) {
1270                        selectionModel.setSelectionInterval(size-1, size-1);
1271                    }
1272                    fireTableDataChanged();
1273                    fireRefresh();
1274                    ensureActiveSelected();
1275                }
1276            });
1277        }
1278
1279        /**
1280         * Invoked when a layer managed by {@link MapView} is added
1281         *
1282         * @param layer the layer
1283         */
1284        protected void onAddLayer(Layer layer) {
1285            if (layer == null) return;
1286            layer.addPropertyChangeListener(this);
1287            fireTableDataChanged();
1288            int idx = getLayers().indexOf(layer);
1289            layerList.setRowHeight(idx, Math.max(16, layer.getIcon().getIconHeight()));
1290            selectionModel.setSelectionInterval(idx, idx);
1291            ensureSelectedIsVisible();
1292        }
1293
1294        /**
1295         * Replies the first layer. Null if no layers are present
1296         *
1297         * @return the first layer. Null if no layers are present
1298         */
1299        public Layer getFirstLayer() {
1300            if (getRowCount() == 0) return null;
1301            return getLayers().get(0);
1302        }
1303
1304        /**
1305         * Replies the layer at position <code>index</code>
1306         *
1307         * @param index the index
1308         * @return the layer at position <code>index</code>. Null,
1309         * if index is out of range.
1310         */
1311        public Layer getLayer(int index) {
1312            if (index < 0 || index >= getRowCount())
1313                return null;
1314            return getLayers().get(index);
1315        }
1316
1317        /**
1318         * Replies true if the currently selected layers can move up
1319         * by one position
1320         *
1321         * @return true if the currently selected layers can move up
1322         * by one position
1323         */
1324        public boolean canMoveUp() {
1325            List<Integer> sel = getSelectedRows();
1326            return !sel.isEmpty() && sel.get(0) > 0;
1327        }
1328
1329        /**
1330         * Move up the currently selected layers by one position
1331         *
1332         */
1333        public void moveUp() {
1334            if (!canMoveUp()) return;
1335            List<Integer> sel = getSelectedRows();
1336            for (int row : sel) {
1337                Layer l1 = getLayers().get(row);
1338                Layer l2 = getLayers().get(row-1);
1339                Main.map.mapView.moveLayer(l2,row);
1340                Main.map.mapView.moveLayer(l1, row-1);
1341            }
1342            fireTableDataChanged();
1343            selectionModel.clearSelection();
1344            for (int row : sel) {
1345                selectionModel.addSelectionInterval(row-1, row-1);
1346            }
1347            ensureSelectedIsVisible();
1348        }
1349
1350        /**
1351         * Replies true if the currently selected layers can move down
1352         * by one position
1353         *
1354         * @return true if the currently selected layers can move down
1355         * by one position
1356         */
1357        public boolean canMoveDown() {
1358            List<Integer> sel = getSelectedRows();
1359            return !sel.isEmpty() && sel.get(sel.size()-1) < getLayers().size()-1;
1360        }
1361
1362        /**
1363         * Move down the currently selected layers by one position
1364         *
1365         */
1366        public void moveDown() {
1367            if (!canMoveDown()) return;
1368            List<Integer> sel = getSelectedRows();
1369            Collections.reverse(sel);
1370            for (int row : sel) {
1371                Layer l1 = getLayers().get(row);
1372                Layer l2 = getLayers().get(row+1);
1373                Main.map.mapView.moveLayer(l1, row+1);
1374                Main.map.mapView.moveLayer(l2, row);
1375            }
1376            fireTableDataChanged();
1377            selectionModel.clearSelection();
1378            for (int row : sel) {
1379                selectionModel.addSelectionInterval(row+1, row+1);
1380            }
1381            ensureSelectedIsVisible();
1382        }
1383
1384        /**
1385         * Make sure the first of the selected layers is visible in the
1386         * views of this model.
1387         *
1388         */
1389        protected void ensureSelectedIsVisible() {
1390            int index = selectionModel.getMinSelectionIndex();
1391            if (index < 0) return;
1392            if (index >= getLayers().size()) return;
1393            Layer layer = getLayers().get(index);
1394            fireMakeVisible(index, layer);
1395        }
1396
1397        /**
1398         * Replies a list of layers which are possible merge targets
1399         * for <code>source</code>
1400         *
1401         * @param source the source layer
1402         * @return a list of layers which are possible merge targets
1403         * for <code>source</code>. Never null, but can be empty.
1404         */
1405        public List<Layer> getPossibleMergeTargets(Layer source) {
1406            List<Layer> targets = new ArrayList<>();
1407            if (source == null)
1408                return targets;
1409            for (Layer target : getLayers()) {
1410                if (source == target) {
1411                    continue;
1412                }
1413                if (target.isMergable(source) && source.isMergable(target)) {
1414                    targets.add(target);
1415                }
1416            }
1417            return targets;
1418        }
1419
1420        /**
1421         * Replies the list of layers currently managed by {@link MapView}.
1422         * Never null, but can be empty.
1423         *
1424         * @return the list of layers currently managed by {@link MapView}.
1425         * Never null, but can be empty.
1426         */
1427        public List<Layer> getLayers() {
1428            if (!Main.isDisplayingMapView())
1429                return Collections.<Layer>emptyList();
1430            return Main.map.mapView.getAllLayersAsList();
1431        }
1432
1433        /**
1434         * Ensures that at least one layer is selected in the layer dialog
1435         *
1436         */
1437        protected void ensureActiveSelected() {
1438            if (getLayers().isEmpty())
1439                return;
1440            final Layer activeLayer = getActiveLayer();
1441            if (activeLayer != null) {
1442                // there's an active layer - select it and make it visible
1443                int idx = getLayers().indexOf(activeLayer);
1444                selectionModel.setSelectionInterval(idx, idx);
1445                ensureSelectedIsVisible();
1446            } else {
1447                // no active layer - select the first one and make it visible
1448                selectionModel.setSelectionInterval(0, 0);
1449                ensureSelectedIsVisible();
1450            }
1451        }
1452
1453        /**
1454         * Replies the active layer. null, if no active layer is available
1455         *
1456         * @return the active layer. null, if no active layer is available
1457         */
1458        protected Layer getActiveLayer() {
1459            if (!Main.isDisplayingMapView()) return null;
1460            return Main.map.mapView.getActiveLayer();
1461        }
1462
1463        /* ------------------------------------------------------------------------------ */
1464        /* Interface TableModel                                                           */
1465        /* ------------------------------------------------------------------------------ */
1466
1467        @Override
1468        public int getRowCount() {
1469            List<Layer> layers = getLayers();
1470            if (layers == null) return 0;
1471            return layers.size();
1472        }
1473
1474        @Override
1475        public int getColumnCount() {
1476            return 3;
1477        }
1478
1479        @Override
1480        public Object getValueAt(int row, int col) {
1481            if (row >= 0 && row < getLayers().size()) {
1482                switch (col) {
1483                case 0: return getLayers().get(row) == getActiveLayer();
1484                case 1: return getLayers().get(row);
1485                case 2: return getLayers().get(row);
1486                default: throw new RuntimeException();
1487                }
1488            }
1489            return null;
1490        }
1491
1492        @Override
1493        public boolean isCellEditable(int row, int col) {
1494            if (col == 0 && getActiveLayer() == getLayers().get(row))
1495                return false;
1496            return true;
1497        }
1498
1499        @Override
1500        public void setValueAt(Object value, int row, int col) {
1501            Layer l = getLayers().get(row);
1502            switch (col) {
1503            case 0:
1504                Main.map.mapView.setActiveLayer(l);
1505                l.setVisible(true);
1506                break;
1507            case 1:
1508                l.setVisible((Boolean) value);
1509                break;
1510            case 2:
1511                l.setName((String) value);
1512                break;
1513            default: throw new RuntimeException();
1514            }
1515            fireTableCellUpdated(row, col);
1516        }
1517
1518        /* ------------------------------------------------------------------------------ */
1519        /* Interface LayerChangeListener                                                  */
1520        /* ------------------------------------------------------------------------------ */
1521        @Override
1522        public void activeLayerChange(final Layer oldLayer, final Layer newLayer) {
1523            GuiHelper.runInEDTAndWait(new Runnable() {
1524                @Override
1525                public void run() {
1526                    if (oldLayer != null) {
1527                        int idx = getLayers().indexOf(oldLayer);
1528                        if (idx >= 0) {
1529                            fireTableRowsUpdated(idx,idx);
1530                        }
1531                    }
1532
1533                    if (newLayer != null) {
1534                        int idx = getLayers().indexOf(newLayer);
1535                        if (idx >= 0) {
1536                            fireTableRowsUpdated(idx,idx);
1537                        }
1538                    }
1539                    ensureActiveSelected();
1540                }
1541            });
1542        }
1543
1544        @Override
1545        public void layerAdded(Layer newLayer) {
1546            onAddLayer(newLayer);
1547        }
1548
1549        @Override
1550        public void layerRemoved(final Layer oldLayer) {
1551            onRemoveLayer(oldLayer);
1552        }
1553
1554        /* ------------------------------------------------------------------------------ */
1555        /* Interface PropertyChangeListener                                               */
1556        /* ------------------------------------------------------------------------------ */
1557        @Override
1558        public void propertyChange(PropertyChangeEvent evt) {
1559            if (evt.getSource() instanceof Layer) {
1560                Layer layer = (Layer)evt.getSource();
1561                final int idx = getLayers().indexOf(layer);
1562                if (idx < 0) return;
1563                fireRefresh();
1564            }
1565        }
1566    }
1567
1568    static class LayerList extends JTable {
1569        public LayerList(TableModel dataModel) {
1570            super(dataModel);
1571        }
1572
1573        public void scrollToVisible(int row, int col) {
1574            if (!(getParent() instanceof JViewport))
1575                return;
1576            JViewport viewport = (JViewport) getParent();
1577            Rectangle rect = getCellRect(row, col, true);
1578            Point pt = viewport.getViewPosition();
1579            rect.setLocation(rect.x - pt.x, rect.y - pt.y);
1580            viewport.scrollRectToVisible(rect);
1581        }
1582    }
1583
1584    /**
1585     * Creates a {@link ShowHideLayerAction} in the
1586     * context of this {@link LayerListDialog}.
1587     *
1588     * @return the action
1589     */
1590    public ShowHideLayerAction createShowHideLayerAction() {
1591        return new ShowHideLayerAction();
1592    }
1593
1594    /**
1595     * Creates a {@link DeleteLayerAction} in the
1596     * context of this {@link LayerListDialog}.
1597     *
1598     * @return the action
1599     */
1600    public DeleteLayerAction createDeleteLayerAction() {
1601        return new DeleteLayerAction();
1602    }
1603
1604    /**
1605     * Creates a {@link ActivateLayerAction} for <code>layer</code> in the
1606     * context of this {@link LayerListDialog}.
1607     *
1608     * @param layer the layer
1609     * @return the action
1610     */
1611    public ActivateLayerAction createActivateLayerAction(Layer layer) {
1612        return new ActivateLayerAction(layer);
1613    }
1614
1615    /**
1616     * Creates a {@link MergeLayerAction} for <code>layer</code> in the
1617     * context of this {@link LayerListDialog}.
1618     *
1619     * @param layer the layer
1620     * @return the action
1621     */
1622    public MergeAction createMergeLayerAction(Layer layer) {
1623        return new MergeAction(layer);
1624    }
1625
1626    /**
1627     * Returns the layer at given index, or {@code null}.
1628     * @param index the index
1629     * @return the layer at given index, or {@code null} if index out of range
1630     */
1631    public static Layer getLayerForIndex(int index) {
1632
1633        if (!Main.isDisplayingMapView())
1634            return null;
1635
1636        List<Layer> layers = Main.map.mapView.getAllLayersAsList();
1637
1638        if (index < layers.size() && index >= 0)
1639            return layers.get(index);
1640        else
1641            return null;
1642    }
1643
1644    /**
1645     * Returns a list of info on all layers of a given class.
1646     * @param layerClass The layer class. This is not {@code Class<? extends Layer>} on purpose,
1647     *                   to allow asking for layers implementing some interface
1648     * @return list of info on all layers assignable from {@code layerClass}
1649     */
1650    public static List<MultikeyInfo> getLayerInfoByClass(Class<?> layerClass) {
1651
1652        List<MultikeyInfo> result = new ArrayList<>();
1653
1654        if (!Main.isDisplayingMapView())
1655            return result;
1656
1657        List<Layer> layers = Main.map.mapView.getAllLayersAsList();
1658
1659        int index = 0;
1660        for (Layer l: layers) {
1661            if (layerClass.isAssignableFrom(l.getClass())) {
1662                result.add(new MultikeyInfo(index, l.getName()));
1663            }
1664            index++;
1665        }
1666
1667        return result;
1668    }
1669
1670    /**
1671     * Determines if a layer is valid (contained in layer list).
1672     * @param l the layer
1673     * @return {@code true} if layer {@code l} is contained in current layer list
1674     */
1675    public static boolean isLayerValid(Layer l) {
1676
1677        if (l == null || !Main.isDisplayingMapView())
1678            return false;
1679
1680        return Main.map.mapView.getAllLayersAsList().contains(l);
1681    }
1682
1683    /**
1684     * Returns info about layer.
1685     * @param l the layer
1686     * @return info about layer {@code l}
1687     */
1688    public static MultikeyInfo getLayerInfo(Layer l) {
1689
1690        if (l == null || !Main.isDisplayingMapView())
1691            return null;
1692
1693        int index = Main.map.mapView.getAllLayersAsList().indexOf(l);
1694        if (index < 0)
1695            return null;
1696
1697        return new MultikeyInfo(index, l.getName());
1698    }
1699}