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