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.Graphics;
011import java.awt.Graphics2D;
012import java.awt.GraphicsEnvironment;
013import java.awt.RenderingHints;
014import java.awt.event.ActionEvent;
015import java.awt.event.InputEvent;
016import java.awt.event.KeyEvent;
017import java.awt.event.MouseEvent;
018import java.beans.PropertyChangeEvent;
019import java.beans.PropertyChangeListener;
020import java.util.ArrayList;
021import java.util.Arrays;
022import java.util.List;
023import java.util.Locale;
024import java.util.concurrent.CopyOnWriteArrayList;
025import java.util.stream.Collectors;
026import java.util.stream.IntStream;
027
028import javax.swing.AbstractAction;
029import javax.swing.DefaultCellEditor;
030import javax.swing.DefaultListSelectionModel;
031import javax.swing.DropMode;
032import javax.swing.Icon;
033import javax.swing.ImageIcon;
034import javax.swing.JCheckBox;
035import javax.swing.JComponent;
036import javax.swing.JLabel;
037import javax.swing.JTable;
038import javax.swing.KeyStroke;
039import javax.swing.ListSelectionModel;
040import javax.swing.UIManager;
041import javax.swing.table.AbstractTableModel;
042import javax.swing.table.DefaultTableCellRenderer;
043import javax.swing.table.TableCellRenderer;
044import javax.swing.table.TableModel;
045
046import org.openstreetmap.josm.actions.ExpertToggleAction;
047import org.openstreetmap.josm.actions.ExpertToggleAction.ExpertModeChangeListener;
048import org.openstreetmap.josm.actions.MergeLayerAction;
049import org.openstreetmap.josm.data.coor.EastNorth;
050import org.openstreetmap.josm.data.imagery.OffsetBookmark;
051import org.openstreetmap.josm.data.preferences.AbstractProperty.ValueChangeEvent;
052import org.openstreetmap.josm.data.preferences.AbstractProperty.ValueChangeListener;
053import org.openstreetmap.josm.data.preferences.BooleanProperty;
054import org.openstreetmap.josm.gui.MainApplication;
055import org.openstreetmap.josm.gui.MapFrame;
056import org.openstreetmap.josm.gui.MapView;
057import org.openstreetmap.josm.gui.SideButton;
058import org.openstreetmap.josm.gui.dialogs.layer.ActivateLayerAction;
059import org.openstreetmap.josm.gui.dialogs.layer.CycleLayerDownAction;
060import org.openstreetmap.josm.gui.dialogs.layer.CycleLayerUpAction;
061import org.openstreetmap.josm.gui.dialogs.layer.DeleteLayerAction;
062import org.openstreetmap.josm.gui.dialogs.layer.DuplicateAction;
063import org.openstreetmap.josm.gui.dialogs.layer.LayerListTransferHandler;
064import org.openstreetmap.josm.gui.dialogs.layer.LayerVisibilityAction;
065import org.openstreetmap.josm.gui.dialogs.layer.MergeAction;
066import org.openstreetmap.josm.gui.dialogs.layer.MoveDownAction;
067import org.openstreetmap.josm.gui.dialogs.layer.MoveUpAction;
068import org.openstreetmap.josm.gui.dialogs.layer.ShowHideLayerAction;
069import org.openstreetmap.josm.gui.layer.AbstractTileSourceLayer;
070import org.openstreetmap.josm.gui.layer.JumpToMarkerActions;
071import org.openstreetmap.josm.gui.layer.Layer;
072import org.openstreetmap.josm.gui.layer.LayerManager.LayerAddEvent;
073import org.openstreetmap.josm.gui.layer.LayerManager.LayerChangeListener;
074import org.openstreetmap.josm.gui.layer.LayerManager.LayerOrderChangeEvent;
075import org.openstreetmap.josm.gui.layer.LayerManager.LayerRemoveEvent;
076import org.openstreetmap.josm.gui.layer.MainLayerManager;
077import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeEvent;
078import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeListener;
079import org.openstreetmap.josm.gui.layer.NativeScaleLayer;
080import org.openstreetmap.josm.gui.layer.imagery.TileSourceDisplaySettings;
081import org.openstreetmap.josm.gui.layer.imagery.TileSourceDisplaySettings.DisplaySettingsChangeEvent;
082import org.openstreetmap.josm.gui.layer.imagery.TileSourceDisplaySettings.DisplaySettingsChangeListener;
083import org.openstreetmap.josm.gui.util.MultikeyActionsHandler;
084import org.openstreetmap.josm.gui.util.MultikeyShortcutAction.MultikeyInfo;
085import org.openstreetmap.josm.gui.util.ReorderableTableModel;
086import org.openstreetmap.josm.gui.util.TableHelper;
087import org.openstreetmap.josm.gui.widgets.DisableShortcutsOnFocusGainedTextField;
088import org.openstreetmap.josm.gui.widgets.JosmTextField;
089import org.openstreetmap.josm.gui.widgets.PopupMenuLauncher;
090import org.openstreetmap.josm.gui.widgets.ScrollableTable;
091import org.openstreetmap.josm.spi.preferences.Config;
092import org.openstreetmap.josm.tools.ImageProvider;
093import org.openstreetmap.josm.tools.ImageProvider.ImageSizes;
094import org.openstreetmap.josm.tools.InputMapUtils;
095import org.openstreetmap.josm.tools.PlatformManager;
096import org.openstreetmap.josm.tools.Shortcut;
097
098/**
099 * This is a toggle dialog which displays the list of layers. Actions allow to
100 * change the ordering of the layers, to hide/show layers, to activate layers,
101 * and to delete layers.
102 * <p>
103 * Support for multiple {@link LayerListDialog} is currently not complete but intended for the future.
104 * @since 17
105 */
106public class LayerListDialog extends ToggleDialog implements DisplaySettingsChangeListener {
107    /** the unique instance of the dialog */
108    private static volatile LayerListDialog instance;
109
110    private static final BooleanProperty DISPLAY_NUMBERS = new BooleanProperty("layerlist.display.numbers", true);
111
112    /**
113     * Creates the instance of the dialog. It's connected to the layer manager
114     *
115     * @param layerManager the layer manager
116     * @since 11885 (signature)
117     */
118    public static void createInstance(MainLayerManager layerManager) {
119        if (instance != null)
120            throw new IllegalStateException("Dialog was already created");
121        instance = new LayerListDialog(layerManager);
122    }
123
124    /**
125     * Replies the instance of the dialog
126     *
127     * @return the instance of the dialog
128     * @throws IllegalStateException if the dialog is not created yet
129     * @see #createInstance(MainLayerManager)
130     */
131    public static LayerListDialog getInstance() {
132        if (instance == null)
133            throw new IllegalStateException("Dialog not created yet. Invoke createInstance() first");
134        return instance;
135    }
136
137    /** the model for the layer list */
138    private final LayerListModel model;
139
140    /** the list of layers (technically its a JTable, but appears like a list) */
141    private final LayerList layerList;
142    private final ColumnWidthAdaptionListener visibilityWidthListener;
143
144    private final ActivateLayerAction activateLayerAction;
145    private final ShowHideLayerAction showHideLayerAction;
146
147    private final CycleLayerUpAction cycleLayerUpAction;
148    private final CycleLayerDownAction cycleLayerDownAction;
149
150    //TODO This duplicates ShowHide actions functionality
151    /** stores which layer index to toggle and executes the ShowHide action if the layer is present */
152    private final class ToggleLayerIndexVisibility extends AbstractAction {
153        private final int layerIndex;
154
155        ToggleLayerIndexVisibility(int layerIndex) {
156            this.layerIndex = layerIndex;
157        }
158
159        @Override
160        public void actionPerformed(ActionEvent e) {
161            final Layer l = model.getLayer(model.getRowCount() - layerIndex - 1);
162            if (l != null) {
163                l.toggleVisible();
164            }
165        }
166    }
167
168    private final transient Shortcut[] visibilityToggleShortcuts = new Shortcut[10];
169    private final ToggleLayerIndexVisibility[] visibilityToggleActions = new ToggleLayerIndexVisibility[10];
170
171    /**
172     * The {@link MainLayerManager} this list is for.
173     */
174    private final transient MainLayerManager layerManager;
175
176    private PopupMenuHandler popupHandler;
177
178    private LayerListModelListener modelListener;
179
180    /**
181     * registers (shortcut to toggle right hand side toggle dialogs)+(number keys) shortcuts
182     * to toggle the visibility of the first ten layers.
183     */
184    private void createVisibilityToggleShortcuts() {
185        for (int i = 0; i < 10; i++) {
186            final int i1 = i + 1;
187            /* POSSIBLE SHORTCUTS: 1,2,3,4,5,6,7,8,9,0=10 */
188            visibilityToggleShortcuts[i] = Shortcut.registerShortcut("subwindow:layers:toggleLayer" + i1,
189                    tr("Toggle visibility of layer: {0}", i1), KeyEvent.VK_0 + (i1 % 10), Shortcut.ALT);
190            visibilityToggleActions[i] = new ToggleLayerIndexVisibility(i);
191            MainApplication.registerActionShortcut(visibilityToggleActions[i], visibilityToggleShortcuts[i]);
192        }
193    }
194
195    /**
196     * Creates a layer list and attach it to the given layer manager.
197     * @param layerManager The layer manager this list is for
198     * @since 10467
199     */
200    public LayerListDialog(MainLayerManager layerManager) {
201        super(tr("Layers"), "layerlist", tr("Open a list of all loaded layers."),
202                Shortcut.registerShortcut("subwindow:layers", tr("Windows: {0}", tr("Layers")), KeyEvent.VK_L,
203                        Shortcut.ALT_SHIFT), 100, true);
204        this.layerManager = layerManager;
205
206        // create the models
207        //
208        DefaultListSelectionModel selectionModel = new DefaultListSelectionModel();
209        selectionModel.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
210        model = new LayerListModel(layerManager, selectionModel);
211
212        // create the list control
213        //
214        layerList = new LayerList(model);
215        TableHelper.setFont(layerList, getClass());
216        layerList.setSelectionModel(selectionModel);
217        popupHandler = new PopupMenuHandler();
218        layerList.addMouseListener(popupHandler);
219        layerList.setBackground(UIManager.getColor("Button.background"));
220        layerList.putClientProperty("terminateEditOnFocusLost", Boolean.TRUE);
221        layerList.putClientProperty("JTable.autoStartsEdit", Boolean.FALSE);
222        layerList.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
223        layerList.setTableHeader(null);
224        layerList.setShowGrid(false);
225        layerList.setIntercellSpacing(new Dimension(0, 0));
226        layerList.getColumnModel().getColumn(0).setCellRenderer(new ActiveLayerCellRenderer());
227        layerList.getColumnModel().getColumn(0).setCellEditor(new DefaultCellEditor(new ActiveLayerCheckBox()));
228        layerList.getColumnModel().getColumn(0).setMaxWidth(12);
229        layerList.getColumnModel().getColumn(0).setPreferredWidth(12);
230        layerList.getColumnModel().getColumn(0).setResizable(false);
231
232        layerList.getColumnModel().getColumn(1).setCellRenderer(new NativeScaleLayerCellRenderer());
233        layerList.getColumnModel().getColumn(1).setCellEditor(new DefaultCellEditor(new NativeScaleLayerCheckBox()));
234        layerList.getColumnModel().getColumn(1).setMaxWidth(12);
235        layerList.getColumnModel().getColumn(1).setPreferredWidth(12);
236        layerList.getColumnModel().getColumn(1).setResizable(false);
237
238        layerList.getColumnModel().getColumn(2).setCellRenderer(new OffsetLayerCellRenderer());
239        layerList.getColumnModel().getColumn(2).setCellEditor(new DefaultCellEditor(new OffsetLayerCheckBox()));
240        layerList.getColumnModel().getColumn(2).setMaxWidth(16);
241        layerList.getColumnModel().getColumn(2).setPreferredWidth(16);
242        layerList.getColumnModel().getColumn(2).setResizable(false);
243
244        layerList.getColumnModel().getColumn(3).setCellRenderer(new LayerVisibleCellRenderer());
245        layerList.getColumnModel().getColumn(3).setCellEditor(new LayerVisibleCellEditor(new LayerVisibleCheckBox()));
246        layerList.getColumnModel().getColumn(3).setResizable(false);
247
248        layerList.getColumnModel().getColumn(4).setCellRenderer(new LayerNameCellRenderer());
249        layerList.getColumnModel().getColumn(4).setCellEditor(new LayerNameCellEditor(new DisableShortcutsOnFocusGainedTextField()));
250        // Disable some default JTable shortcuts to use JOSM ones (see #5678, #10458)
251        for (KeyStroke ks : new KeyStroke[] {
252                KeyStroke.getKeyStroke(KeyEvent.VK_C, PlatformManager.getPlatform().getMenuShortcutKeyMaskEx()),
253                KeyStroke.getKeyStroke(KeyEvent.VK_V, PlatformManager.getPlatform().getMenuShortcutKeyMaskEx()),
254                KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, InputEvent.SHIFT_DOWN_MASK),
255                KeyStroke.getKeyStroke(KeyEvent.VK_UP, InputEvent.SHIFT_DOWN_MASK),
256                KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, InputEvent.SHIFT_DOWN_MASK),
257                KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, InputEvent.SHIFT_DOWN_MASK),
258                KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, InputEvent.CTRL_DOWN_MASK),
259                KeyStroke.getKeyStroke(KeyEvent.VK_UP, InputEvent.CTRL_DOWN_MASK),
260                KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, InputEvent.CTRL_DOWN_MASK),
261                KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, InputEvent.CTRL_DOWN_MASK),
262                KeyStroke.getKeyStroke(KeyEvent.VK_PAGE_UP, 0),
263                KeyStroke.getKeyStroke(KeyEvent.VK_PAGE_DOWN, 0),
264                KeyStroke.getKeyStroke(KeyEvent.VK_TAB, 0),
265                KeyStroke.getKeyStroke(KeyEvent.VK_F8, 0),
266        }) {
267            layerList.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(ks, new Object());
268        }
269
270        visibilityWidthListener = new ColumnWidthAdaptionListener(3, 16);
271        DISPLAY_NUMBERS.addListener(visibilityWidthListener);
272        ExpertToggleAction.addExpertModeChangeListener(visibilityWidthListener);
273        layerManager.addLayerChangeListener(visibilityWidthListener);
274        visibilityWidthListener.updateColumnWidth();
275
276        // init the model
277        //
278        model.populate();
279        model.setSelectedLayer(layerManager.getActiveLayer());
280        modelListener = new LayerListModelListener() {
281            @Override
282            public void makeVisible(int row, Layer layer) {
283                layerList.scrollToVisible(row, 0);
284                layerList.repaint();
285            }
286
287            @Override
288            public void refresh() {
289                layerList.repaint();
290            }
291        };
292
293        model.addLayerListModelListener(modelListener);
294
295        // -- move up action
296        MoveUpAction moveUpAction = new MoveUpAction(model);
297        TableHelper.adaptTo(moveUpAction, model);
298        TableHelper.adaptTo(moveUpAction, selectionModel);
299
300        // -- move down action
301        MoveDownAction moveDownAction = new MoveDownAction(model);
302        TableHelper.adaptTo(moveDownAction, model);
303        TableHelper.adaptTo(moveDownAction, selectionModel);
304
305        // -- activate action
306        activateLayerAction = new ActivateLayerAction(model);
307        activateLayerAction.updateEnabledState();
308        MultikeyActionsHandler.getInstance().addAction(activateLayerAction);
309        TableHelper.adaptTo(activateLayerAction, selectionModel);
310
311        JumpToMarkerActions.initialize();
312
313        // -- show hide action
314        showHideLayerAction = new ShowHideLayerAction(model);
315        MultikeyActionsHandler.getInstance().addAction(showHideLayerAction);
316        TableHelper.adaptTo(showHideLayerAction, selectionModel);
317
318        LayerVisibilityAction visibilityAction = new LayerVisibilityAction(model);
319        TableHelper.adaptTo(visibilityAction, selectionModel);
320        SideButton visibilityButton = new SideButton(visibilityAction, false);
321        visibilityAction.setCorrespondingSideButton(visibilityButton);
322
323        // -- delete layer action
324        DeleteLayerAction deleteLayerAction = new DeleteLayerAction(model);
325        layerList.getActionMap().put("deleteLayer", deleteLayerAction);
326        TableHelper.adaptTo(deleteLayerAction, selectionModel);
327        getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(
328                KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0), "delete"
329                );
330        getActionMap().put("delete", deleteLayerAction);
331
332        // Activate layer on Enter key press
333        InputMapUtils.addEnterAction(layerList, new AbstractAction() {
334            @Override
335            public void actionPerformed(ActionEvent e) {
336                activateLayerAction.actionPerformed(null);
337                layerList.requestFocus();
338            }
339        });
340
341        // Show/Activate layer on Enter key press
342        InputMapUtils.addSpacebarAction(layerList, showHideLayerAction);
343
344        // Cycle layer actions
345        cycleLayerUpAction = new CycleLayerUpAction();
346        cycleLayerDownAction = new CycleLayerDownAction();
347
348        createLayout(layerList, true, Arrays.asList(
349                new SideButton(moveUpAction, false),
350                new SideButton(moveDownAction, false),
351                new SideButton(activateLayerAction, false),
352                visibilityButton,
353                new SideButton(deleteLayerAction, false)
354        ));
355
356        createVisibilityToggleShortcuts();
357    }
358
359    private static boolean displayLayerNumbers() {
360        return ExpertToggleAction.isExpert() && DISPLAY_NUMBERS.get();
361    }
362
363    /**
364     * Gets the layer manager this dialog is for.
365     * @return The layer manager.
366     * @since 10288
367     */
368    public MainLayerManager getLayerManager() {
369        return layerManager;
370    }
371
372    @Override
373    public void showNotify() {
374        layerManager.addActiveLayerChangeListener(activateLayerAction);
375        layerManager.addAndFireLayerChangeListener(model);
376        layerManager.addAndFireActiveLayerChangeListener(model);
377        model.populate();
378    }
379
380    @Override
381    public void hideNotify() {
382        layerManager.removeAndFireLayerChangeListener(model);
383        layerManager.removeActiveLayerChangeListener(model);
384        layerManager.removeActiveLayerChangeListener(activateLayerAction);
385    }
386
387    /**
388     * Returns the layer list model.
389     * @return the layer list model
390     */
391    public LayerListModel getModel() {
392        return model;
393    }
394
395    @Override
396    public void destroy() {
397        for (int i = 0; i < 10; i++) {
398            MainApplication.unregisterActionShortcut(visibilityToggleActions[i], visibilityToggleShortcuts[i]);
399        }
400        MultikeyActionsHandler.getInstance().removeAction(activateLayerAction);
401        MultikeyActionsHandler.getInstance().removeAction(showHideLayerAction);
402        JumpToMarkerActions.unregisterActions();
403        layerList.setTransferHandler(null);
404        layerList.removeMouseListener(popupHandler);
405        DISPLAY_NUMBERS.removeListener(visibilityWidthListener);
406        ExpertToggleAction.removeExpertModeChangeListener(visibilityWidthListener);
407        layerManager.removeLayerChangeListener(visibilityWidthListener);
408        activateLayerAction.destroy();
409        cycleLayerUpAction.destroy();
410        cycleLayerDownAction.destroy();
411        model.removeLayerListModelListener(modelListener);
412        super.destroy();
413        instance = null;
414    }
415
416    static ImageIcon createBlankIcon() {
417        return ImageProvider.createBlankIcon(ImageSizes.LAYER);
418    }
419
420    private class ColumnWidthAdaptionListener implements ValueChangeListener<Boolean>, ExpertModeChangeListener, LayerChangeListener {
421        private final int minWidth;
422        private final int column;
423
424        ColumnWidthAdaptionListener(int column, int minWidth) {
425            this.column = column;
426            this.minWidth = minWidth;
427        }
428
429        @Override
430        public void expertChanged(boolean isExpert) {
431            updateColumnWidth();
432        }
433
434        @Override
435        public void valueChanged(ValueChangeEvent<? extends Boolean> e) {
436            updateColumnWidth();
437        }
438
439        @Override
440        public void layerAdded(LayerAddEvent e) {
441            updateColumnWidth();
442        }
443
444        @Override
445        public void layerRemoving(LayerRemoveEvent e) {
446            updateColumnWidth();
447        }
448
449        @Override
450        public void layerOrderChanged(LayerOrderChangeEvent e) {
451            //not needed
452        }
453
454        public void updateColumnWidth() {
455            int width = minWidth;
456            for (int row = 0; row < layerList.getRowCount(); row++) {
457                TableCellRenderer renderer = layerList.getCellRenderer(row, column);
458                Component comp = layerList.prepareRenderer(renderer, row, column);
459                width = Math.max(comp.getPreferredSize().width + 1, width);
460            }
461            layerList.getColumnModel().getColumn(column).setMaxWidth(width);
462            layerList.getColumnModel().getColumn(column).setPreferredWidth(width);
463            repaint();
464        }
465    }
466
467    private static class ActiveLayerCheckBox extends JCheckBox {
468        ActiveLayerCheckBox() {
469            setHorizontalAlignment(javax.swing.SwingConstants.CENTER);
470            ImageIcon blank = createBlankIcon();
471            ImageIcon active = ImageProvider.get("dialogs/layerlist", "active");
472            setIcon(blank);
473            setSelectedIcon(active);
474            setRolloverIcon(blank);
475            setRolloverSelectedIcon(active);
476        }
477    }
478
479    private static class LayerVisibleCheckBox extends JCheckBox {
480        private final ImageIcon iconEye;
481        private final ImageIcon iconEyeTranslucent;
482        private boolean isTranslucent;
483        private Layer layer;
484
485        /**
486         * Constructs a new {@code LayerVisibleCheckBox}.
487         */
488        LayerVisibleCheckBox() {
489            iconEye = new EyeIcon(/* ICON(dialogs/layerlist/) */ "eye");
490            iconEyeTranslucent = new EyeIcon(/* ICON(dialogs/layerlist/) */ "eye-translucent", true);
491            setIcon(ImageProvider.get("dialogs/layerlist", "eye-off"));
492            setSelectedIcon(iconEye);
493            isTranslucent = false;
494        }
495
496        public void setTranslucent(boolean isTranslucent) {
497            if (this.isTranslucent == isTranslucent) return;
498            if (isTranslucent) {
499                setSelectedIcon(iconEyeTranslucent);
500            } else {
501                setSelectedIcon(iconEye);
502            }
503            this.isTranslucent = isTranslucent;
504        }
505
506        public void updateStatus(Layer layer) {
507            this.layer = layer;
508            boolean visible = layer.isVisible();
509            setSelected(visible);
510            if (displayLayerNumbers()) {
511                List<Layer> layers = MainApplication.getLayerManager().getLayers();
512                int num = layers.size() - layers.indexOf(layer);
513                setText(String.format("%s[%d]", num < 10 ? " " : "", num));
514            } else {
515                setText(null);
516            }
517            setTranslucent(layer.getOpacity() < 1.0);
518            setToolTipText(visible ?
519                tr("layer is currently visible (click to hide layer)") :
520                tr("layer is currently hidden (click to show layer)"));
521        }
522
523        private class EyeIcon extends ImageIcon {
524            private final boolean translucent;
525
526            EyeIcon(String name) {
527                this(name, false);
528            }
529
530            EyeIcon(String name, boolean translucent) {
531                super(ImageProvider.get("dialogs/layerlist", name).getImage());
532                this.translucent = translucent;
533            }
534
535            @Override
536            public synchronized void paintIcon(Component comp, Graphics g, int x, int y) {
537                Color c;
538                if (Config.getPref().getBoolean("dialog.layer.colorname", true)
539                        && layer != null && (c = layer.getColor()) != null) {
540                    if (g instanceof Graphics2D) {
541                        ((Graphics2D) g).setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
542                    }
543                    if (translucent) {
544                        g.setColor(new Color(c.getRed(), c.getGreen(), c.getBlue(), 125));
545                    } else {
546                        g.setColor(c);
547                    }
548                    g.fillOval(x, y + 2, getIconWidth(), getIconHeight() - 3);
549                }
550                super.paintIcon(comp, g, x, y);
551            }
552        }
553    }
554
555    private static class NativeScaleLayerCheckBox extends JCheckBox {
556        NativeScaleLayerCheckBox() {
557            setHorizontalAlignment(javax.swing.SwingConstants.CENTER);
558            ImageIcon blank = createBlankIcon();
559            ImageIcon active = ImageProvider.get("dialogs/layerlist", "scale");
560            setIcon(blank);
561            setSelectedIcon(active);
562        }
563    }
564
565    private static class OffsetLayerCheckBox extends JCheckBox {
566        OffsetLayerCheckBox() {
567            setHorizontalAlignment(javax.swing.SwingConstants.CENTER);
568            ImageIcon blank = createBlankIcon();
569            ImageIcon withOffset = ImageProvider.get("dialogs/layerlist", "offset");
570            setIcon(blank);
571            setSelectedIcon(withOffset);
572        }
573    }
574
575    private static class ActiveLayerCellRenderer implements TableCellRenderer {
576        private final JCheckBox cb;
577
578        /**
579         * Constructs a new {@code ActiveLayerCellRenderer}.
580         */
581        ActiveLayerCellRenderer() {
582            cb = new ActiveLayerCheckBox();
583        }
584
585        @Override
586        public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
587            boolean active = value != null && (Boolean) value;
588            cb.setSelected(active);
589            cb.setToolTipText(active ? tr("this layer is the active layer") : tr("this layer is not currently active (click to activate)"));
590            return cb;
591        }
592    }
593
594    private static class LayerVisibleCellRenderer implements TableCellRenderer {
595        private final LayerVisibleCheckBox cb;
596
597        /**
598         * Constructs a new {@code LayerVisibleCellRenderer}.
599         */
600        LayerVisibleCellRenderer() {
601            this.cb = new LayerVisibleCheckBox();
602        }
603
604        @Override
605        public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
606            if (value != null) {
607                cb.updateStatus((Layer) value);
608            }
609            return cb;
610        }
611    }
612
613    private static class LayerVisibleCellEditor extends DefaultCellEditor {
614        private final LayerVisibleCheckBox cb;
615
616        LayerVisibleCellEditor(LayerVisibleCheckBox cb) {
617            super(cb);
618            this.cb = cb;
619        }
620
621        @Override
622        public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) {
623            cb.updateStatus((Layer) value);
624            return cb;
625        }
626    }
627
628    private static class NativeScaleLayerCellRenderer implements TableCellRenderer {
629        private final JCheckBox cb;
630
631        /**
632         * Constructs a new {@code ActiveLayerCellRenderer}.
633         */
634        NativeScaleLayerCellRenderer() {
635            cb = new NativeScaleLayerCheckBox();
636        }
637
638        @Override
639        public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
640            Layer layer = (Layer) value;
641            if (layer instanceof NativeScaleLayer) {
642                boolean active = ((NativeScaleLayer) layer) == MainApplication.getMap().mapView.getNativeScaleLayer();
643                cb.setSelected(active);
644                if (MainApplication.getMap().mapView.getNativeScaleLayer() != null) {
645                    cb.setToolTipText(active
646                            ? tr("scale follows native resolution of this layer")
647                            : tr("scale follows native resolution of another layer (click to set this layer)"));
648                } else {
649                    cb.setToolTipText(tr("scale does not follow native resolution of any layer (click to set this layer)"));
650                }
651            } else {
652                cb.setSelected(false);
653                cb.setToolTipText(tr("this layer has no native resolution"));
654            }
655            return cb;
656        }
657    }
658
659    private static class OffsetLayerCellRenderer implements TableCellRenderer {
660        private final JCheckBox cb;
661
662        /**
663         * Constructs a new {@code OffsetLayerCellRenderer}.
664         */
665        OffsetLayerCellRenderer() {
666            cb = new OffsetLayerCheckBox();
667            cb.setEnabled(false);
668        }
669
670        @Override
671        public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
672            Layer layer = (Layer) value;
673            if (layer instanceof AbstractTileSourceLayer<?>) {
674                final TileSourceDisplaySettings displaySettings = ((AbstractTileSourceLayer<?>) layer).getDisplaySettings();
675                if (EastNorth.ZERO.equals(displaySettings.getDisplacement())) {
676                    final boolean hasPreviousOffset = displaySettings.getPreviousOffsetBookmark() != null;
677                    cb.setSelected(false);
678                    cb.setEnabled(hasPreviousOffset);
679                    cb.setToolTipText(tr("layer is without a user-defined offset") +
680                            (hasPreviousOffset ? " " + tr("(click to activate previous offset)") : ""));
681                } else {
682                    cb.setSelected(true);
683                    cb.setEnabled(true);
684                    cb.setToolTipText(tr("layer has an offset of {0} (click to remove offset)",
685                            displaySettings.getDisplacementString(Locale.getDefault())));
686                }
687
688            } else {
689                cb.setSelected(false);
690                cb.setEnabled(false);
691                cb.setToolTipText(tr("this layer can not have an offset"));
692            }
693            return cb;
694        }
695    }
696
697    private class LayerNameCellRenderer extends DefaultTableCellRenderer {
698
699        protected boolean isActiveLayer(Layer layer) {
700            return getLayerManager().getActiveLayer() == layer;
701        }
702
703        @Override
704        public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
705            if (value == null)
706                return this;
707            Layer layer = (Layer) value;
708            JLabel label = (JLabel) super.getTableCellRendererComponent(table,
709                    layer.getLabel(), isSelected, hasFocus, row, column);
710            if (isActiveLayer(layer)) {
711                label.setFont(label.getFont().deriveFont(Font.BOLD));
712            }
713            label.setIcon(layer.getIcon());
714            label.setToolTipText(layer.getToolTipText());
715            return label;
716        }
717    }
718
719    private static class LayerNameCellEditor extends DefaultCellEditor {
720        LayerNameCellEditor(DisableShortcutsOnFocusGainedTextField tf) {
721            super(tf);
722        }
723
724        @Override
725        public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) {
726            JosmTextField tf = (JosmTextField) super.getTableCellEditorComponent(table, value, isSelected, row, column);
727            tf.setText(value == null ? "" : ((Layer) value).getName());
728            return tf;
729        }
730    }
731
732    class PopupMenuHandler extends PopupMenuLauncher {
733        @Override
734        public void showMenu(MouseEvent evt) {
735            menu = new LayerListPopup(getModel().getSelectedLayers());
736            super.showMenu(evt);
737        }
738    }
739
740    /**
741     * Observer interface to be implemented by views using {@link LayerListModel}.
742     */
743    public interface LayerListModelListener {
744
745        /**
746         * Fired when a layer is made visible.
747         * @param index the layer index
748         * @param layer the layer
749         */
750        void makeVisible(int index, Layer layer);
751
752
753        /**
754         * Fired when something has changed in the layer list model.
755         */
756        void refresh();
757    }
758
759    /**
760     * The layer list model. The model manages a list of layers and provides methods for
761     * moving layers up and down, for toggling their visibility, and for activating a layer.
762     *
763     * The model is a {@link TableModel} and it provides a {@link ListSelectionModel}. It expects
764     * to be configured with a {@link DefaultListSelectionModel}. The selection model is used
765     * to update the selection state of views depending on messages sent to the model.
766     *
767     * The model manages a list of {@link LayerListModelListener} which are mainly notified if
768     * the model requires views to make a specific list entry visible.
769     *
770     * It also listens to {@link PropertyChangeEvent}s of every {@link Layer} it manages, in particular to
771     * the properties {@link Layer#VISIBLE_PROP} and {@link Layer#NAME_PROP}.
772     */
773    public static final class LayerListModel extends AbstractTableModel
774            implements LayerChangeListener, ActiveLayerChangeListener, PropertyChangeListener, ReorderableTableModel<Layer> {
775        /** manages list selection state*/
776        private final DefaultListSelectionModel selectionModel;
777        private final CopyOnWriteArrayList<LayerListModelListener> listeners;
778        private LayerList layerList;
779        private final MainLayerManager layerManager;
780
781        /**
782         * constructor
783         * @param layerManager The layer manager to use for the list.
784         * @param selectionModel the list selection model
785         */
786        LayerListModel(MainLayerManager layerManager, DefaultListSelectionModel selectionModel) {
787            this.layerManager = layerManager;
788            this.selectionModel = selectionModel;
789            listeners = new CopyOnWriteArrayList<>();
790        }
791
792        void setLayerList(LayerList layerList) {
793            this.layerList = layerList;
794        }
795
796        /**
797         * The layer manager this model is for.
798         * @return The layer manager.
799         */
800        public MainLayerManager getLayerManager() {
801            return layerManager;
802        }
803
804        /**
805         * Adds a listener to this model
806         *
807         * @param listener the listener
808         */
809        public void addLayerListModelListener(LayerListModelListener listener) {
810            if (listener != null) {
811                listeners.addIfAbsent(listener);
812            }
813        }
814
815        /**
816         * removes a listener from  this model
817         * @param listener the listener
818         */
819        public void removeLayerListModelListener(LayerListModelListener listener) {
820            listeners.remove(listener);
821        }
822
823        /**
824         * Fires a make visible event to listeners
825         *
826         * @param index the index of the row to make visible
827         * @param layer the layer at this index
828         * @see LayerListModelListener#makeVisible(int, Layer)
829         */
830        private void fireMakeVisible(int index, Layer layer) {
831            for (LayerListModelListener listener : listeners) {
832                listener.makeVisible(index, layer);
833            }
834        }
835
836        /**
837         * Fires a refresh event to listeners of this model
838         *
839         * @see LayerListModelListener#refresh()
840         */
841        private void fireRefresh() {
842            for (LayerListModelListener listener : listeners) {
843                listener.refresh();
844            }
845        }
846
847        /**
848         * Populates the model with the current layers managed by {@link MapView}.
849         */
850        public void populate() {
851            for (Layer layer: getLayers()) {
852                // make sure the model is registered exactly once
853                layer.removePropertyChangeListener(this);
854                layer.addPropertyChangeListener(this);
855            }
856            fireTableDataChanged();
857        }
858
859        /**
860         * Marks <code>layer</code> as selected layer. Ignored, if layer is null.
861         *
862         * @param layer the layer.
863         */
864        public void setSelectedLayer(Layer layer) {
865            if (layer == null)
866                return;
867            int idx = getLayers().indexOf(layer);
868            if (idx >= 0) {
869                selectionModel.setSelectionInterval(idx, idx);
870            }
871            ensureSelectedIsVisible();
872        }
873
874        /**
875         * Replies the list of currently selected layers. Never null, but may be empty.
876         *
877         * @return the list of currently selected layers. Never null, but may be empty.
878         */
879        public List<Layer> getSelectedLayers() {
880            List<Layer> layers = getLayers();
881            return IntStream.range(0, layers.size())
882                    .filter(selectionModel::isSelectedIndex)
883                    .mapToObj(layers::get)
884                    .collect(Collectors.toList());
885        }
886
887        /**
888         * Replies a the list of indices of the selected rows. Never null, but may be empty.
889         *
890         * @return  the list of indices of the selected rows. Never null, but may be empty.
891         */
892        public List<Integer> getSelectedRows() {
893            return TableHelper.selectedIndices(selectionModel).boxed().collect(Collectors.toList());
894        }
895
896        /**
897         * Invoked if a layer managed by {@link MapView} is removed
898         *
899         * @param layer the layer which is removed
900         */
901        private void onRemoveLayer(Layer layer) {
902            if (layer == null)
903                return;
904            layer.removePropertyChangeListener(this);
905            final int size = getRowCount();
906
907            if (selectionModel.isSelectionEmpty() && size > 0) {
908                selectionModel.setSelectionInterval(size-1, size-1);
909            }
910            fireTableDataChanged();
911            fireRefresh();
912            ensureActiveSelected();
913            if (layer instanceof AbstractTileSourceLayer<?>) {
914                ((AbstractTileSourceLayer<?>) layer).getDisplaySettings().removeSettingsChangeListener(LayerListDialog.getInstance());
915            }
916
917        }
918
919        /**
920         * Invoked when a layer managed by {@link MapView} is added
921         *
922         * @param layer the layer
923         */
924        private void onAddLayer(Layer layer) {
925            if (layer == null)
926                return;
927            layer.addPropertyChangeListener(this);
928            fireTableDataChanged();
929            int idx = getLayers().indexOf(layer);
930            Icon icon = layer.getIcon();
931            if (layerList != null && icon != null) {
932                layerList.setRowHeight(idx, Math.max(layerList.getRowHeight(), icon.getIconHeight()));
933            }
934            selectionModel.setSelectionInterval(idx, idx);
935            ensureSelectedIsVisible();
936            if (layer instanceof AbstractTileSourceLayer<?>) {
937                ((AbstractTileSourceLayer<?>) layer).getDisplaySettings().addSettingsChangeListener(LayerListDialog.getInstance());
938            }
939        }
940
941        /**
942         * Replies the first layer. Null if no layers are present
943         *
944         * @return the first layer. Null if no layers are present
945         */
946        public Layer getFirstLayer() {
947            if (getRowCount() == 0)
948                return null;
949            return getLayers().get(0);
950        }
951
952        /**
953         * Replies the layer at position <code>index</code>
954         *
955         * @param index the index
956         * @return the layer at position <code>index</code>. Null,
957         * if index is out of range.
958         */
959        public Layer getLayer(int index) {
960            if (index < 0 || index >= getRowCount())
961                return null;
962            return getLayers().get(index);
963        }
964
965        @Override
966        public DefaultListSelectionModel getSelectionModel() {
967            return selectionModel;
968        }
969
970        @Override
971        public Layer getValue(int index) {
972            return getLayer(index);
973        }
974
975        @Override
976        public Layer setValue(int index, Layer value) {
977            throw new UnsupportedOperationException();
978        }
979
980        @Override
981        public boolean doMove(int delta, int... selectedRows) {
982            if (delta != 0) {
983                List<Layer> layers = getLayers();
984                MapView mapView = MainApplication.getMap().mapView;
985                if (delta < 0) {
986                    for (int row : selectedRows) {
987                        mapView.moveLayer(layers.get(row), row + delta);
988                    }
989                } else {
990                    for (int i = selectedRows.length - 1; i >= 0; i--) {
991                        mapView.moveLayer(layers.get(selectedRows[i]), selectedRows[i] + delta);
992                    }
993                }
994                fireTableDataChanged();
995            }
996            return delta != 0;
997        }
998
999        @Override
1000        public boolean move(int delta, int... selectedRows) {
1001            if (!ReorderableTableModel.super.move(delta, selectedRows))
1002                return false;
1003            ensureSelectedIsVisible();
1004            return true;
1005        }
1006
1007        /**
1008         * Make sure the first of the selected layers is visible in the views of this model.
1009         */
1010        private void ensureSelectedIsVisible() {
1011            int index = selectionModel.getMinSelectionIndex();
1012            if (index < 0)
1013                return;
1014            List<Layer> layers = getLayers();
1015            if (index >= layers.size())
1016                return;
1017            Layer layer = layers.get(index);
1018            fireMakeVisible(index, layer);
1019        }
1020
1021        /**
1022         * Replies a list of layers which are possible merge targets for <code>source</code>
1023         *
1024         * @param source the source layer
1025         * @return a list of layers which are possible merge targets
1026         * for <code>source</code>. Never null, but can be empty.
1027         */
1028        public List<Layer> getPossibleMergeTargets(Layer source) {
1029            if (source == null) {
1030                return new ArrayList<>();
1031            }
1032            return getLayers().stream()
1033                    .filter(target -> source != target && target.isMergable(source) && source.isMergable(target))
1034                    .collect(Collectors.toList());
1035        }
1036
1037        /**
1038         * Replies the list of layers currently managed by {@link MapView}.
1039         * Never null, but can be empty.
1040         *
1041         * @return the list of layers currently managed by {@link MapView}.
1042         * Never null, but can be empty.
1043         */
1044        public List<Layer> getLayers() {
1045            return getLayerManager().getLayers();
1046        }
1047
1048        /**
1049         * Ensures that at least one layer is selected in the layer dialog
1050         *
1051         */
1052        private void ensureActiveSelected() {
1053            List<Layer> layers = getLayers();
1054            if (layers.isEmpty())
1055                return;
1056            final Layer activeLayer = getActiveLayer();
1057            if (activeLayer != null) {
1058                // there's an active layer - select it and make it visible
1059                int idx = layers.indexOf(activeLayer);
1060                selectionModel.setSelectionInterval(idx, idx);
1061                ensureSelectedIsVisible();
1062            } else {
1063                // no active layer - select the first one and make it visible
1064                selectionModel.setSelectionInterval(0, 0);
1065                ensureSelectedIsVisible();
1066            }
1067        }
1068
1069        /**
1070         * Replies the active layer. null, if no active layer is available
1071         *
1072         * @return the active layer. null, if no active layer is available
1073         */
1074        private Layer getActiveLayer() {
1075            return getLayerManager().getActiveLayer();
1076        }
1077
1078        /* ------------------------------------------------------------------------------ */
1079        /* Interface TableModel                                                           */
1080        /* ------------------------------------------------------------------------------ */
1081
1082        @Override
1083        public int getRowCount() {
1084            List<Layer> layers = getLayers();
1085            return layers == null ? 0 : layers.size();
1086        }
1087
1088        @Override
1089        public int getColumnCount() {
1090            return 5;
1091        }
1092
1093        @Override
1094        public Object getValueAt(int row, int col) {
1095            List<Layer> layers = getLayers();
1096            if (row >= 0 && row < layers.size()) {
1097                switch (col) {
1098                case 0: return layers.get(row) == getActiveLayer();
1099                case 1:
1100                case 2:
1101                case 3:
1102                case 4: return layers.get(row);
1103                default: // Do nothing
1104                }
1105            }
1106            return null;
1107        }
1108
1109        @Override
1110        public boolean isCellEditable(int row, int col) {
1111            return col != 0 || getActiveLayer() != getLayers().get(row);
1112        }
1113
1114        @Override
1115        public void setValueAt(Object value, int row, int col) {
1116            List<Layer> layers = getLayers();
1117            if (row < layers.size()) {
1118                Layer l = layers.get(row);
1119                switch (col) {
1120                case 0:
1121                    getLayerManager().setActiveLayer(l);
1122                    l.setVisible(true);
1123                    break;
1124                case 1:
1125                    MapFrame map = MainApplication.getMap();
1126                    NativeScaleLayer oldLayer = map.mapView.getNativeScaleLayer();
1127                    if (oldLayer == l) {
1128                        map.mapView.setNativeScaleLayer(null);
1129                    } else if (l instanceof NativeScaleLayer) {
1130                        map.mapView.setNativeScaleLayer((NativeScaleLayer) l);
1131                        if (oldLayer instanceof Layer) {
1132                            int idx = getLayers().indexOf((Layer) oldLayer);
1133                            if (idx >= 0) {
1134                                fireTableCellUpdated(idx, col);
1135                            }
1136                        }
1137                    }
1138                    break;
1139                case 2:
1140                    // reset layer offset
1141                    if (l instanceof AbstractTileSourceLayer<?>) {
1142                        final TileSourceDisplaySettings displaySettings = ((AbstractTileSourceLayer<?>) l).getDisplaySettings();
1143                        final OffsetBookmark offsetBookmark = displaySettings.getOffsetBookmark();
1144                        if (offsetBookmark != null) {
1145                            displaySettings.setOffsetBookmark(null);
1146                            MainApplication.getMenu().imageryMenu.refreshOffsetMenu();
1147                        } else {
1148                            displaySettings.setOffsetBookmark(displaySettings.getPreviousOffsetBookmark());
1149                        }
1150                    }
1151                    break;
1152                case 3:
1153                    l.setVisible((Boolean) value);
1154                    break;
1155                case 4:
1156                    l.rename((String) value);
1157                    break;
1158                default:
1159                    throw new IllegalArgumentException("Wrong column: " + col);
1160                }
1161                fireTableCellUpdated(row, col);
1162            }
1163        }
1164
1165        /* ------------------------------------------------------------------------------ */
1166        /* Interface ActiveLayerChangeListener                                            */
1167        /* ------------------------------------------------------------------------------ */
1168        @Override
1169        public void activeOrEditLayerChanged(ActiveLayerChangeEvent e) {
1170            Layer oldLayer = e.getPreviousActiveLayer();
1171            if (oldLayer != null) {
1172                int idx = getLayers().indexOf(oldLayer);
1173                if (idx >= 0) {
1174                    fireTableRowsUpdated(idx, idx);
1175                }
1176            }
1177
1178            Layer newLayer = getActiveLayer();
1179            if (newLayer != null) {
1180                int idx = getLayers().indexOf(newLayer);
1181                if (idx >= 0) {
1182                    fireTableRowsUpdated(idx, idx);
1183                }
1184            }
1185            ensureActiveSelected();
1186        }
1187
1188        /* ------------------------------------------------------------------------------ */
1189        /* Interface LayerChangeListener                                                  */
1190        /* ------------------------------------------------------------------------------ */
1191        @Override
1192        public void layerAdded(LayerAddEvent e) {
1193            onAddLayer(e.getAddedLayer());
1194        }
1195
1196        @Override
1197        public void layerRemoving(LayerRemoveEvent e) {
1198            onRemoveLayer(e.getRemovedLayer());
1199        }
1200
1201        @Override
1202        public void layerOrderChanged(LayerOrderChangeEvent e) {
1203            fireTableDataChanged();
1204        }
1205
1206        /* ------------------------------------------------------------------------------ */
1207        /* Interface PropertyChangeListener                                               */
1208        /* ------------------------------------------------------------------------------ */
1209        @Override
1210        public void propertyChange(PropertyChangeEvent evt) {
1211            if (evt.getSource() instanceof Layer) {
1212                Layer layer = (Layer) evt.getSource();
1213                final int idx = getLayers().indexOf(layer);
1214                if (idx < 0)
1215                    return;
1216                fireRefresh();
1217            }
1218        }
1219    }
1220
1221    /**
1222     * This component displays a list of layers and provides the methods needed by {@link LayerListModel}.
1223     */
1224    static class LayerList extends ScrollableTable {
1225
1226        LayerList(LayerListModel dataModel) {
1227            super(dataModel);
1228            dataModel.setLayerList(this);
1229            if (!GraphicsEnvironment.isHeadless()) {
1230                setDragEnabled(true);
1231            }
1232            setDropMode(DropMode.INSERT_ROWS);
1233            setTransferHandler(new LayerListTransferHandler());
1234        }
1235
1236        @Override
1237        public LayerListModel getModel() {
1238            return (LayerListModel) super.getModel();
1239        }
1240    }
1241
1242    /**
1243     * Creates a {@link ShowHideLayerAction} in the context of this {@link LayerListDialog}.
1244     *
1245     * @return the action
1246     */
1247    public ShowHideLayerAction createShowHideLayerAction() {
1248        return new ShowHideLayerAction(model);
1249    }
1250
1251    /**
1252     * Creates a {@link DeleteLayerAction} in the context of this {@link LayerListDialog}.
1253     *
1254     * @return the action
1255     */
1256    public DeleteLayerAction createDeleteLayerAction() {
1257        return new DeleteLayerAction(model);
1258    }
1259
1260    /**
1261     * Creates a {@link ActivateLayerAction} for <code>layer</code> in the context of this {@link LayerListDialog}.
1262     *
1263     * @param layer the layer
1264     * @return the action
1265     */
1266    public ActivateLayerAction createActivateLayerAction(Layer layer) {
1267        return new ActivateLayerAction(layer, model);
1268    }
1269
1270    /**
1271     * Creates a {@link MergeLayerAction} for <code>layer</code> in the context of this {@link LayerListDialog}.
1272     *
1273     * @param layer the layer
1274     * @return the action
1275     */
1276    public MergeAction createMergeLayerAction(Layer layer) {
1277        return new MergeAction(layer, model);
1278    }
1279
1280    /**
1281     * Creates a {@link DuplicateAction} for <code>layer</code> in the context of this {@link LayerListDialog}.
1282     *
1283     * @param layer the layer
1284     * @return the action
1285     */
1286    public DuplicateAction createDuplicateLayerAction(Layer layer) {
1287        return new DuplicateAction(layer, model);
1288    }
1289
1290    /**
1291     * Returns the layer at given index, or {@code null}.
1292     * @param index the index
1293     * @return the layer at given index, or {@code null} if index out of range
1294     */
1295    public static Layer getLayerForIndex(int index) {
1296        List<Layer> layers = MainApplication.getLayerManager().getLayers();
1297
1298        if (index < layers.size() && index >= 0)
1299            return layers.get(index);
1300        else
1301            return null;
1302    }
1303
1304    /**
1305     * Returns a list of info on all layers of a given class.
1306     * @param layerClass The layer class. This is not {@code Class<? extends Layer>} on purpose,
1307     *                   to allow asking for layers implementing some interface
1308     * @return list of info on all layers assignable from {@code layerClass}
1309     */
1310    public static List<MultikeyInfo> getLayerInfoByClass(Class<?> layerClass) {
1311        List<MultikeyInfo> result = new ArrayList<>();
1312
1313        List<Layer> layers = MainApplication.getLayerManager().getLayers();
1314
1315        int index = 0;
1316        for (Layer l: layers) {
1317            if (layerClass.isAssignableFrom(l.getClass())) {
1318                result.add(new MultikeyInfo(index, l.getName()));
1319            }
1320            index++;
1321        }
1322
1323        return result;
1324    }
1325
1326    /**
1327     * Determines if a layer is valid (contained in global layer list).
1328     * @param l the layer
1329     * @return {@code true} if layer {@code l} is contained in current layer list
1330     */
1331    public static boolean isLayerValid(Layer l) {
1332        if (l == null)
1333            return false;
1334
1335        return MainApplication.getLayerManager().containsLayer(l);
1336    }
1337
1338    /**
1339     * Returns info about layer.
1340     * @param l the layer
1341     * @return info about layer {@code l}
1342     */
1343    public static MultikeyInfo getLayerInfo(Layer l) {
1344        if (l == null)
1345            return null;
1346
1347        int index = MainApplication.getLayerManager().getLayers().indexOf(l);
1348        if (index < 0)
1349            return null;
1350
1351        return new MultikeyInfo(index, l.getName());
1352    }
1353
1354    @Override
1355    public void displaySettingsChanged(DisplaySettingsChangeEvent e) {
1356        if ("displacement".equals(e.getChangedSetting())) {
1357            layerList.repaint();
1358        }
1359    }
1360}