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