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