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