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