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