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