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.table.AbstractTableModel;
037import javax.swing.table.DefaultTableCellRenderer;
038import javax.swing.table.TableCellRenderer;
039import javax.swing.table.TableModel;
040
041import org.openstreetmap.josm.actions.MergeLayerAction;
042import org.openstreetmap.josm.data.coor.EastNorth;
043import org.openstreetmap.josm.data.imagery.OffsetBookmark;
044import org.openstreetmap.josm.data.preferences.AbstractProperty;
045import org.openstreetmap.josm.gui.MainApplication;
046import org.openstreetmap.josm.gui.MapFrame;
047import org.openstreetmap.josm.gui.MapView;
048import org.openstreetmap.josm.gui.SideButton;
049import org.openstreetmap.josm.gui.dialogs.layer.ActivateLayerAction;
050import org.openstreetmap.josm.gui.dialogs.layer.DeleteLayerAction;
051import org.openstreetmap.josm.gui.dialogs.layer.DuplicateAction;
052import org.openstreetmap.josm.gui.dialogs.layer.LayerListTransferHandler;
053import org.openstreetmap.josm.gui.dialogs.layer.LayerVisibilityAction;
054import org.openstreetmap.josm.gui.dialogs.layer.MergeAction;
055import org.openstreetmap.josm.gui.dialogs.layer.MoveDownAction;
056import org.openstreetmap.josm.gui.dialogs.layer.MoveUpAction;
057import org.openstreetmap.josm.gui.dialogs.layer.ShowHideLayerAction;
058import org.openstreetmap.josm.gui.layer.AbstractTileSourceLayer;
059import org.openstreetmap.josm.gui.layer.JumpToMarkerActions;
060import org.openstreetmap.josm.gui.layer.Layer;
061import org.openstreetmap.josm.gui.layer.LayerManager.LayerAddEvent;
062import org.openstreetmap.josm.gui.layer.LayerManager.LayerChangeListener;
063import org.openstreetmap.josm.gui.layer.LayerManager.LayerOrderChangeEvent;
064import org.openstreetmap.josm.gui.layer.LayerManager.LayerRemoveEvent;
065import org.openstreetmap.josm.gui.layer.MainLayerManager;
066import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeEvent;
067import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeListener;
068import org.openstreetmap.josm.gui.layer.NativeScaleLayer;
069import org.openstreetmap.josm.gui.layer.imagery.TileSourceDisplaySettings.DisplaySettingsChangeEvent;
070import org.openstreetmap.josm.gui.layer.imagery.TileSourceDisplaySettings.DisplaySettingsChangeListener;
071import org.openstreetmap.josm.gui.util.MultikeyActionsHandler;
072import org.openstreetmap.josm.gui.util.MultikeyShortcutAction.MultikeyInfo;
073import org.openstreetmap.josm.gui.util.ReorderableTableModel;
074import org.openstreetmap.josm.gui.util.TableHelper;
075import org.openstreetmap.josm.gui.widgets.DisableShortcutsOnFocusGainedTextField;
076import org.openstreetmap.josm.gui.widgets.JosmTextField;
077import org.openstreetmap.josm.gui.widgets.PopupMenuLauncher;
078import org.openstreetmap.josm.gui.widgets.ScrollableTable;
079import org.openstreetmap.josm.spi.preferences.Config;
080import org.openstreetmap.josm.tools.ArrayUtils;
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(48);
224        layerList.getColumnModel().getColumn(3).setPreferredWidth(48);
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        TableHelper.adaptTo(moveUpAction, model);
271        TableHelper.adaptTo(moveUpAction, selectionModel);
272
273        // -- move down action
274        MoveDownAction moveDownAction = new MoveDownAction(model);
275        TableHelper.adaptTo(moveDownAction, model);
276        TableHelper.adaptTo(moveDownAction, selectionModel);
277
278        // -- activate action
279        activateLayerAction = new ActivateLayerAction(model);
280        activateLayerAction.updateEnabledState();
281        MultikeyActionsHandler.getInstance().addAction(activateLayerAction);
282        TableHelper.adaptTo(activateLayerAction, selectionModel);
283
284        JumpToMarkerActions.initialize();
285
286        // -- show hide action
287        showHideLayerAction = new ShowHideLayerAction(model);
288        MultikeyActionsHandler.getInstance().addAction(showHideLayerAction);
289        TableHelper.adaptTo(showHideLayerAction, selectionModel);
290
291        LayerVisibilityAction visibilityAction = new LayerVisibilityAction(model);
292        TableHelper.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        TableHelper.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    @Override
361    public void destroy() {
362        for (int i = 0; i < 10; i++) {
363            MainApplication.unregisterActionShortcut(visibilityToggleActions[i], visibilityToggleShortcuts[i]);
364        }
365        MultikeyActionsHandler.getInstance().removeAction(activateLayerAction);
366        MultikeyActionsHandler.getInstance().removeAction(showHideLayerAction);
367        JumpToMarkerActions.unregisterActions();
368        layerList.setTransferHandler(null);
369        super.destroy();
370        instance = null;
371    }
372
373    static ImageIcon createBlankIcon() {
374        return ImageProvider.createBlankIcon(ImageSizes.LAYER);
375    }
376
377    private static class ActiveLayerCheckBox extends JCheckBox {
378        ActiveLayerCheckBox() {
379            setHorizontalAlignment(javax.swing.SwingConstants.CENTER);
380            ImageIcon blank = createBlankIcon();
381            ImageIcon active = ImageProvider.get("dialogs/layerlist", "active");
382            setIcon(blank);
383            setSelectedIcon(active);
384            setRolloverIcon(blank);
385            setRolloverSelectedIcon(active);
386            setPressedIcon(ImageProvider.get("dialogs/layerlist", "active-pressed"));
387        }
388    }
389
390    private static class LayerVisibleCheckBox extends JCheckBox {
391        private final ImageIcon iconEye;
392        private final ImageIcon iconEyeTranslucent;
393        private boolean isTranslucent;
394
395        /**
396         * Constructs a new {@code LayerVisibleCheckBox}.
397         */
398        LayerVisibleCheckBox() {
399            setHorizontalAlignment(javax.swing.SwingConstants.RIGHT);
400            iconEye = ImageProvider.get("dialogs/layerlist", "eye");
401            iconEyeTranslucent = ImageProvider.get("dialogs/layerlist", "eye-translucent");
402            setIcon(ImageProvider.get("dialogs/layerlist", "eye-off"));
403            setPressedIcon(ImageProvider.get("dialogs/layerlist", "eye-pressed"));
404            setSelectedIcon(iconEye);
405            isTranslucent = false;
406        }
407
408        public void setTranslucent(boolean isTranslucent) {
409            if (this.isTranslucent == isTranslucent) return;
410            if (isTranslucent) {
411                setSelectedIcon(iconEyeTranslucent);
412            } else {
413                setSelectedIcon(iconEye);
414            }
415            this.isTranslucent = isTranslucent;
416        }
417
418        public void updateStatus(Layer layer) {
419            boolean visible = layer.isVisible();
420            setSelected(visible);
421            List<Layer> layers = MainApplication.getLayerManager().getLayers();
422            int num = layers.size() - layers.indexOf(layer);
423            setText(String.format("%s[%d]", num < 10 ? " " : "", num));
424            setTranslucent(layer.getOpacity() < 1.0);
425            setToolTipText(visible ?
426                tr("layer is currently visible (click to hide layer)") :
427                tr("layer is currently hidden (click to show layer)"));
428        }
429    }
430
431    private static class NativeScaleLayerCheckBox extends JCheckBox {
432        NativeScaleLayerCheckBox() {
433            setHorizontalAlignment(javax.swing.SwingConstants.CENTER);
434            ImageIcon blank = createBlankIcon();
435            ImageIcon active = ImageProvider.get("dialogs/layerlist", "scale");
436            setIcon(blank);
437            setSelectedIcon(active);
438        }
439    }
440
441    private static class OffsetLayerCheckBox extends JCheckBox {
442        OffsetLayerCheckBox() {
443            setHorizontalAlignment(javax.swing.SwingConstants.CENTER);
444            ImageIcon blank = createBlankIcon();
445            ImageIcon withOffset = ImageProvider.get("dialogs/layerlist", "offset");
446            setIcon(blank);
447            setSelectedIcon(withOffset);
448        }
449    }
450
451    private static class ActiveLayerCellRenderer implements TableCellRenderer {
452        private final JCheckBox cb;
453
454        /**
455         * Constructs a new {@code ActiveLayerCellRenderer}.
456         */
457        ActiveLayerCellRenderer() {
458            cb = new ActiveLayerCheckBox();
459        }
460
461        @Override
462        public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
463            boolean active = value != null && (Boolean) value;
464            cb.setSelected(active);
465            cb.setToolTipText(active ? tr("this layer is the active layer") : tr("this layer is not currently active (click to activate)"));
466            return cb;
467        }
468    }
469
470    private static class LayerVisibleCellRenderer implements TableCellRenderer {
471        private final LayerVisibleCheckBox cb;
472
473        /**
474         * Constructs a new {@code LayerVisibleCellRenderer}.
475         */
476        LayerVisibleCellRenderer() {
477            this.cb = new LayerVisibleCheckBox();
478        }
479
480        @Override
481        public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
482            if (value != null) {
483                cb.updateStatus((Layer) value);
484            }
485            return cb;
486        }
487    }
488
489    private static class LayerVisibleCellEditor extends DefaultCellEditor {
490        private final LayerVisibleCheckBox cb;
491
492        LayerVisibleCellEditor(LayerVisibleCheckBox cb) {
493            super(cb);
494            this.cb = cb;
495        }
496
497        @Override
498        public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) {
499            cb.updateStatus((Layer) value);
500            return cb;
501        }
502    }
503
504    private static class NativeScaleLayerCellRenderer implements TableCellRenderer {
505        private final JCheckBox cb;
506
507        /**
508         * Constructs a new {@code ActiveLayerCellRenderer}.
509         */
510        NativeScaleLayerCellRenderer() {
511            cb = new NativeScaleLayerCheckBox();
512        }
513
514        @Override
515        public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
516            Layer layer = (Layer) value;
517            if (layer instanceof NativeScaleLayer) {
518                boolean active = ((NativeScaleLayer) layer) == MainApplication.getMap().mapView.getNativeScaleLayer();
519                cb.setSelected(active);
520                if (MainApplication.getMap().mapView.getNativeScaleLayer() != null) {
521                    cb.setToolTipText(active
522                            ? tr("scale follows native resolution of this layer")
523                            : tr("scale follows native resolution of another layer (click to set this layer)"));
524                } else {
525                    cb.setToolTipText(tr("scale does not follow native resolution of any layer (click to set this layer)"));
526                }
527            } else {
528                cb.setSelected(false);
529                cb.setToolTipText(tr("this layer has no native resolution"));
530            }
531            return cb;
532        }
533    }
534
535    private static class OffsetLayerCellRenderer implements TableCellRenderer {
536        private final JCheckBox cb;
537
538        /**
539         * Constructs a new {@code OffsetLayerCellRenderer}.
540         */
541        OffsetLayerCellRenderer() {
542            cb = new OffsetLayerCheckBox();
543            cb.setEnabled(false);
544        }
545
546        @Override
547        public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
548            Layer layer = (Layer) value;
549            if (layer instanceof AbstractTileSourceLayer<?>) {
550                if (EastNorth.ZERO.equals(((AbstractTileSourceLayer<?>) layer).getDisplaySettings().getDisplacement())) {
551                    cb.setSelected(false);
552                    cb.setEnabled(false); // TODO: allow reselecting checkbox and thereby setting the old offset again
553                    cb.setToolTipText(tr("layer is without a user-defined offset"));
554                } else {
555                    cb.setSelected(true);
556                    cb.setEnabled(true);
557                    cb.setToolTipText(tr("layer has a user-defined offset (click to remove offset)"));
558                }
559
560            } else {
561                cb.setSelected(false);
562                cb.setEnabled(false);
563                cb.setToolTipText(tr("this layer can not have an offset"));
564            }
565            return cb;
566        }
567    }
568
569    private class LayerNameCellRenderer extends DefaultTableCellRenderer {
570
571        protected boolean isActiveLayer(Layer layer) {
572            return getLayerManager().getActiveLayer() == layer;
573        }
574
575        @Override
576        public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
577            if (value == null)
578                return this;
579            Layer layer = (Layer) value;
580            JLabel label = (JLabel) super.getTableCellRendererComponent(table,
581                    layer.getName(), isSelected, hasFocus, row, column);
582            if (isActiveLayer(layer)) {
583                label.setFont(label.getFont().deriveFont(Font.BOLD));
584            }
585            if (Config.getPref().getBoolean("dialog.layer.colorname", true)) {
586                AbstractProperty<Color> prop = layer.getColorProperty();
587                Color c = prop == null ? null : prop.get();
588                if (c == null || model.getLayers().stream()
589                        .map(Layer::getColorProperty)
590                        .filter(Objects::nonNull)
591                        .map(AbstractProperty::get)
592                        .noneMatch(oc -> oc != null && !oc.equals(c))) {
593                    /* not more than one color, don't use coloring */
594                    label.setForeground(UIManager.getColor(isSelected ? "Table.selectionForeground" : "Table.foreground"));
595                } else {
596                    label.setForeground(c);
597                }
598            }
599            label.setIcon(layer.getIcon());
600            label.setToolTipText(layer.getToolTipText());
601            return label;
602        }
603    }
604
605    private static class LayerNameCellEditor extends DefaultCellEditor {
606        LayerNameCellEditor(DisableShortcutsOnFocusGainedTextField tf) {
607            super(tf);
608        }
609
610        @Override
611        public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) {
612            JosmTextField tf = (JosmTextField) super.getTableCellEditorComponent(table, value, isSelected, row, column);
613            tf.setText(value == null ? "" : ((Layer) value).getName());
614            return tf;
615        }
616    }
617
618    class PopupMenuHandler extends PopupMenuLauncher {
619        @Override
620        public void showMenu(MouseEvent evt) {
621            menu = new LayerListPopup(getModel().getSelectedLayers());
622            super.showMenu(evt);
623        }
624    }
625
626    /**
627     * Observer interface to be implemented by views using {@link LayerListModel}.
628     */
629    public interface LayerListModelListener {
630
631        /**
632         * Fired when a layer is made visible.
633         * @param index the layer index
634         * @param layer the layer
635         */
636        void makeVisible(int index, Layer layer);
637
638
639        /**
640         * Fired when something has changed in the layer list model.
641         */
642        void refresh();
643    }
644
645    /**
646     * The layer list model. The model manages a list of layers and provides methods for
647     * moving layers up and down, for toggling their visibility, and for activating a layer.
648     *
649     * The model is a {@link TableModel} and it provides a {@link ListSelectionModel}. It expects
650     * to be configured with a {@link DefaultListSelectionModel}. The selection model is used
651     * to update the selection state of views depending on messages sent to the model.
652     *
653     * The model manages a list of {@link LayerListModelListener} which are mainly notified if
654     * the model requires views to make a specific list entry visible.
655     *
656     * It also listens to {@link PropertyChangeEvent}s of every {@link Layer} it manages, in particular to
657     * the properties {@link Layer#VISIBLE_PROP} and {@link Layer#NAME_PROP}.
658     */
659    public static final class LayerListModel extends AbstractTableModel
660            implements LayerChangeListener, ActiveLayerChangeListener, PropertyChangeListener, ReorderableTableModel<Layer> {
661        /** manages list selection state*/
662        private final DefaultListSelectionModel selectionModel;
663        private final CopyOnWriteArrayList<LayerListModelListener> listeners;
664        private LayerList layerList;
665        private final MainLayerManager layerManager;
666
667        /**
668         * constructor
669         * @param layerManager The layer manager to use for the list.
670         * @param selectionModel the list selection model
671         */
672        LayerListModel(MainLayerManager layerManager, DefaultListSelectionModel selectionModel) {
673            this.layerManager = layerManager;
674            this.selectionModel = selectionModel;
675            listeners = new CopyOnWriteArrayList<>();
676        }
677
678        void setLayerList(LayerList layerList) {
679            this.layerList = layerList;
680        }
681
682        /**
683         * The layer manager this model is for.
684         * @return The layer manager.
685         */
686        public MainLayerManager getLayerManager() {
687            return layerManager;
688        }
689
690        /**
691         * Adds a listener to this model
692         *
693         * @param listener the listener
694         */
695        public void addLayerListModelListener(LayerListModelListener listener) {
696            if (listener != null) {
697                listeners.addIfAbsent(listener);
698            }
699        }
700
701        /**
702         * removes a listener from  this model
703         * @param listener the listener
704         */
705        public void removeLayerListModelListener(LayerListModelListener listener) {
706            listeners.remove(listener);
707        }
708
709        /**
710         * Fires a make visible event to listeners
711         *
712         * @param index the index of the row to make visible
713         * @param layer the layer at this index
714         * @see LayerListModelListener#makeVisible(int, Layer)
715         */
716        private void fireMakeVisible(int index, Layer layer) {
717            for (LayerListModelListener listener : listeners) {
718                listener.makeVisible(index, layer);
719            }
720        }
721
722        /**
723         * Fires a refresh event to listeners of this model
724         *
725         * @see LayerListModelListener#refresh()
726         */
727        private void fireRefresh() {
728            for (LayerListModelListener listener : listeners) {
729                listener.refresh();
730            }
731        }
732
733        /**
734         * Populates the model with the current layers managed by {@link MapView}.
735         */
736        public void populate() {
737            for (Layer layer: getLayers()) {
738                // make sure the model is registered exactly once
739                layer.removePropertyChangeListener(this);
740                layer.addPropertyChangeListener(this);
741            }
742            fireTableDataChanged();
743        }
744
745        /**
746         * Marks <code>layer</code> as selected layer. Ignored, if layer is null.
747         *
748         * @param layer the layer.
749         */
750        public void setSelectedLayer(Layer layer) {
751            if (layer == null)
752                return;
753            int idx = getLayers().indexOf(layer);
754            if (idx >= 0) {
755                selectionModel.setSelectionInterval(idx, idx);
756            }
757            ensureSelectedIsVisible();
758        }
759
760        /**
761         * Replies the list of currently selected layers. Never null, but may be empty.
762         *
763         * @return the list of currently selected layers. Never null, but may be empty.
764         */
765        public List<Layer> getSelectedLayers() {
766            List<Layer> selected = new ArrayList<>();
767            List<Layer> layers = getLayers();
768            for (int i = 0; i < layers.size(); i++) {
769                if (selectionModel.isSelectedIndex(i)) {
770                    selected.add(layers.get(i));
771                }
772            }
773            return selected;
774        }
775
776        /**
777         * Replies a the list of indices of the selected rows. Never null, but may be empty.
778         *
779         * @return  the list of indices of the selected rows. Never null, but may be empty.
780         */
781        public List<Integer> getSelectedRows() {
782            return ArrayUtils.toList(TableHelper.getSelectedIndices(selectionModel));
783        }
784
785        /**
786         * Invoked if a layer managed by {@link MapView} is removed
787         *
788         * @param layer the layer which is removed
789         */
790        private void onRemoveLayer(Layer layer) {
791            if (layer == null)
792                return;
793            layer.removePropertyChangeListener(this);
794            final int size = getRowCount();
795            final int[] rows = TableHelper.getSelectedIndices(selectionModel);
796
797            if (rows.length == 0 && size > 0) {
798                selectionModel.setSelectionInterval(size-1, size-1);
799            }
800            fireTableDataChanged();
801            fireRefresh();
802            ensureActiveSelected();
803        }
804
805        /**
806         * Invoked when a layer managed by {@link MapView} is added
807         *
808         * @param layer the layer
809         */
810        private void onAddLayer(Layer layer) {
811            if (layer == null)
812                return;
813            layer.addPropertyChangeListener(this);
814            fireTableDataChanged();
815            int idx = getLayers().indexOf(layer);
816            Icon icon = layer.getIcon();
817            if (layerList != null && icon != null) {
818                layerList.setRowHeight(idx, Math.max(16, icon.getIconHeight()));
819            }
820            selectionModel.setSelectionInterval(idx, idx);
821            ensureSelectedIsVisible();
822            if (layer instanceof AbstractTileSourceLayer<?>) {
823                ((AbstractTileSourceLayer<?>) layer).getDisplaySettings().addSettingsChangeListener(LayerListDialog.getInstance());
824            }
825        }
826
827        /**
828         * Replies the first layer. Null if no layers are present
829         *
830         * @return the first layer. Null if no layers are present
831         */
832        public Layer getFirstLayer() {
833            if (getRowCount() == 0)
834                return null;
835            return getLayers().get(0);
836        }
837
838        /**
839         * Replies the layer at position <code>index</code>
840         *
841         * @param index the index
842         * @return the layer at position <code>index</code>. Null,
843         * if index is out of range.
844         */
845        public Layer getLayer(int index) {
846            if (index < 0 || index >= getRowCount())
847                return null;
848            return getLayers().get(index);
849        }
850
851        @Override
852        public DefaultListSelectionModel getSelectionModel() {
853            return selectionModel;
854        }
855
856        @Override
857        public Layer getValue(int index) {
858            return getLayer(index);
859        }
860
861        @Override
862        public Layer setValue(int index, Layer value) {
863            throw new UnsupportedOperationException();
864        }
865
866        @Override
867        public boolean doMove(int delta, int... selectedRows) {
868            if (delta != 0) {
869                List<Layer> layers = getLayers();
870                MapView mapView = MainApplication.getMap().mapView;
871                if (delta < 0) {
872                    for (int row : selectedRows) {
873                        mapView.moveLayer(layers.get(row), row + delta);
874                    }
875                } else {
876                    for (int i = selectedRows.length - 1; i >= 0; i--) {
877                        mapView.moveLayer(layers.get(selectedRows[i]), selectedRows[i] + delta);
878                    }
879                }
880                fireTableDataChanged();
881            }
882            return delta != 0;
883        }
884
885        @Override
886        public boolean move(int delta, int... selectedRows) {
887            if (!ReorderableTableModel.super.move(delta, selectedRows))
888                return false;
889            ensureSelectedIsVisible();
890            return true;
891        }
892
893        /**
894         * Make sure the first of the selected layers is visible in the views of this model.
895         */
896        private void ensureSelectedIsVisible() {
897            int index = selectionModel.getMinSelectionIndex();
898            if (index < 0)
899                return;
900            List<Layer> layers = getLayers();
901            if (index >= layers.size())
902                return;
903            Layer layer = layers.get(index);
904            fireMakeVisible(index, layer);
905        }
906
907        /**
908         * Replies a list of layers which are possible merge targets for <code>source</code>
909         *
910         * @param source the source layer
911         * @return a list of layers which are possible merge targets
912         * for <code>source</code>. Never null, but can be empty.
913         */
914        public List<Layer> getPossibleMergeTargets(Layer source) {
915            List<Layer> targets = new ArrayList<>();
916            if (source == null) {
917                return targets;
918            }
919            for (Layer target : getLayers()) {
920                if (source == target) {
921                    continue;
922                }
923                if (target.isMergable(source) && source.isMergable(target)) {
924                    targets.add(target);
925                }
926            }
927            return targets;
928        }
929
930        /**
931         * Replies the list of layers currently managed by {@link MapView}.
932         * Never null, but can be empty.
933         *
934         * @return the list of layers currently managed by {@link MapView}.
935         * Never null, but can be empty.
936         */
937        public List<Layer> getLayers() {
938            return getLayerManager().getLayers();
939        }
940
941        /**
942         * Ensures that at least one layer is selected in the layer dialog
943         *
944         */
945        private void ensureActiveSelected() {
946            List<Layer> layers = getLayers();
947            if (layers.isEmpty())
948                return;
949            final Layer activeLayer = getActiveLayer();
950            if (activeLayer != null) {
951                // there's an active layer - select it and make it visible
952                int idx = layers.indexOf(activeLayer);
953                selectionModel.setSelectionInterval(idx, idx);
954                ensureSelectedIsVisible();
955            } else {
956                // no active layer - select the first one and make it visible
957                selectionModel.setSelectionInterval(0, 0);
958                ensureSelectedIsVisible();
959            }
960        }
961
962        /**
963         * Replies the active layer. null, if no active layer is available
964         *
965         * @return the active layer. null, if no active layer is available
966         */
967        private Layer getActiveLayer() {
968            return getLayerManager().getActiveLayer();
969        }
970
971        /* ------------------------------------------------------------------------------ */
972        /* Interface TableModel                                                           */
973        /* ------------------------------------------------------------------------------ */
974
975        @Override
976        public int getRowCount() {
977            List<Layer> layers = getLayers();
978            return layers == null ? 0 : layers.size();
979        }
980
981        @Override
982        public int getColumnCount() {
983            return 5;
984        }
985
986        @Override
987        public Object getValueAt(int row, int col) {
988            List<Layer> layers = getLayers();
989            if (row >= 0 && row < layers.size()) {
990                switch (col) {
991                case 0: return layers.get(row) == getActiveLayer();
992                case 1:
993                case 2:
994                case 3:
995                case 4: return layers.get(row);
996                default: // Do nothing
997                }
998            }
999            return null;
1000        }
1001
1002        @Override
1003        public boolean isCellEditable(int row, int col) {
1004            return col != 0 || getActiveLayer() != getLayers().get(row);
1005        }
1006
1007        @Override
1008        public void setValueAt(Object value, int row, int col) {
1009            List<Layer> layers = getLayers();
1010            if (row < layers.size()) {
1011                Layer l = layers.get(row);
1012                switch (col) {
1013                case 0:
1014                    getLayerManager().setActiveLayer(l);
1015                    l.setVisible(true);
1016                    break;
1017                case 1:
1018                    MapFrame map = MainApplication.getMap();
1019                    NativeScaleLayer oldLayer = map.mapView.getNativeScaleLayer();
1020                    if (oldLayer == l) {
1021                        map.mapView.setNativeScaleLayer(null);
1022                    } else if (l instanceof NativeScaleLayer) {
1023                        map.mapView.setNativeScaleLayer((NativeScaleLayer) l);
1024                        if (oldLayer instanceof Layer) {
1025                            int idx = getLayers().indexOf((Layer) oldLayer);
1026                            if (idx >= 0) {
1027                                fireTableCellUpdated(idx, col);
1028                            }
1029                        }
1030                    }
1031                    break;
1032                case 2:
1033                    // reset layer offset
1034                    if (l instanceof AbstractTileSourceLayer<?>) {
1035                        AbstractTileSourceLayer<?> abstractTileSourceLayer = (AbstractTileSourceLayer<?>) l;
1036                        OffsetBookmark offsetBookmark = abstractTileSourceLayer.getDisplaySettings().getOffsetBookmark();
1037                        if (offsetBookmark != null) {
1038                            abstractTileSourceLayer.getDisplaySettings().setOffsetBookmark(null);
1039                            MainApplication.getMenu().imageryMenu.refreshOffsetMenu();
1040                        }
1041                    }
1042                    break;
1043                case 3:
1044                    l.setVisible((Boolean) value);
1045                    break;
1046                case 4:
1047                    l.rename((String) value);
1048                    break;
1049                default:
1050                    throw new IllegalArgumentException("Wrong column: " + col);
1051                }
1052                fireTableCellUpdated(row, col);
1053            }
1054        }
1055
1056        /* ------------------------------------------------------------------------------ */
1057        /* Interface ActiveLayerChangeListener                                            */
1058        /* ------------------------------------------------------------------------------ */
1059        @Override
1060        public void activeOrEditLayerChanged(ActiveLayerChangeEvent e) {
1061            Layer oldLayer = e.getPreviousActiveLayer();
1062            if (oldLayer != null) {
1063                int idx = getLayers().indexOf(oldLayer);
1064                if (idx >= 0) {
1065                    fireTableRowsUpdated(idx, idx);
1066                }
1067            }
1068
1069            Layer newLayer = getActiveLayer();
1070            if (newLayer != null) {
1071                int idx = getLayers().indexOf(newLayer);
1072                if (idx >= 0) {
1073                    fireTableRowsUpdated(idx, idx);
1074                }
1075            }
1076            ensureActiveSelected();
1077        }
1078
1079        /* ------------------------------------------------------------------------------ */
1080        /* Interface LayerChangeListener                                                  */
1081        /* ------------------------------------------------------------------------------ */
1082        @Override
1083        public void layerAdded(LayerAddEvent e) {
1084            onAddLayer(e.getAddedLayer());
1085        }
1086
1087        @Override
1088        public void layerRemoving(LayerRemoveEvent e) {
1089            onRemoveLayer(e.getRemovedLayer());
1090        }
1091
1092        @Override
1093        public void layerOrderChanged(LayerOrderChangeEvent e) {
1094            fireTableDataChanged();
1095        }
1096
1097        /* ------------------------------------------------------------------------------ */
1098        /* Interface PropertyChangeListener                                               */
1099        /* ------------------------------------------------------------------------------ */
1100        @Override
1101        public void propertyChange(PropertyChangeEvent evt) {
1102            if (evt.getSource() instanceof Layer) {
1103                Layer layer = (Layer) evt.getSource();
1104                final int idx = getLayers().indexOf(layer);
1105                if (idx < 0)
1106                    return;
1107                fireRefresh();
1108            }
1109        }
1110    }
1111
1112    /**
1113     * This component displays a list of layers and provides the methods needed by {@link LayerListModel}.
1114     */
1115    static class LayerList extends ScrollableTable {
1116
1117        LayerList(LayerListModel dataModel) {
1118            super(dataModel);
1119            dataModel.setLayerList(this);
1120            if (!GraphicsEnvironment.isHeadless()) {
1121                setDragEnabled(true);
1122            }
1123            setDropMode(DropMode.INSERT_ROWS);
1124            setTransferHandler(new LayerListTransferHandler());
1125        }
1126
1127        @Override
1128        public LayerListModel getModel() {
1129            return (LayerListModel) super.getModel();
1130        }
1131    }
1132
1133    /**
1134     * Creates a {@link ShowHideLayerAction} in the context of this {@link LayerListDialog}.
1135     *
1136     * @return the action
1137     */
1138    public ShowHideLayerAction createShowHideLayerAction() {
1139        return new ShowHideLayerAction(model);
1140    }
1141
1142    /**
1143     * Creates a {@link DeleteLayerAction} in the context of this {@link LayerListDialog}.
1144     *
1145     * @return the action
1146     */
1147    public DeleteLayerAction createDeleteLayerAction() {
1148        return new DeleteLayerAction(model);
1149    }
1150
1151    /**
1152     * Creates a {@link ActivateLayerAction} for <code>layer</code> in the context of this {@link LayerListDialog}.
1153     *
1154     * @param layer the layer
1155     * @return the action
1156     */
1157    public ActivateLayerAction createActivateLayerAction(Layer layer) {
1158        return new ActivateLayerAction(layer, model);
1159    }
1160
1161    /**
1162     * Creates a {@link MergeLayerAction} for <code>layer</code> in the context of this {@link LayerListDialog}.
1163     *
1164     * @param layer the layer
1165     * @return the action
1166     */
1167    public MergeAction createMergeLayerAction(Layer layer) {
1168        return new MergeAction(layer, model);
1169    }
1170
1171    /**
1172     * Creates a {@link DuplicateAction} for <code>layer</code> in the context of this {@link LayerListDialog}.
1173     *
1174     * @param layer the layer
1175     * @return the action
1176     */
1177    public DuplicateAction createDuplicateLayerAction(Layer layer) {
1178        return new DuplicateAction(layer, model);
1179    }
1180
1181    /**
1182     * Returns the layer at given index, or {@code null}.
1183     * @param index the index
1184     * @return the layer at given index, or {@code null} if index out of range
1185     */
1186    public static Layer getLayerForIndex(int index) {
1187        List<Layer> layers = MainApplication.getLayerManager().getLayers();
1188
1189        if (index < layers.size() && index >= 0)
1190            return layers.get(index);
1191        else
1192            return null;
1193    }
1194
1195    /**
1196     * Returns a list of info on all layers of a given class.
1197     * @param layerClass The layer class. This is not {@code Class<? extends Layer>} on purpose,
1198     *                   to allow asking for layers implementing some interface
1199     * @return list of info on all layers assignable from {@code layerClass}
1200     */
1201    public static List<MultikeyInfo> getLayerInfoByClass(Class<?> layerClass) {
1202        List<MultikeyInfo> result = new ArrayList<>();
1203
1204        List<Layer> layers = MainApplication.getLayerManager().getLayers();
1205
1206        int index = 0;
1207        for (Layer l: layers) {
1208            if (layerClass.isAssignableFrom(l.getClass())) {
1209                result.add(new MultikeyInfo(index, l.getName()));
1210            }
1211            index++;
1212        }
1213
1214        return result;
1215    }
1216
1217    /**
1218     * Determines if a layer is valid (contained in global layer list).
1219     * @param l the layer
1220     * @return {@code true} if layer {@code l} is contained in current layer list
1221     */
1222    public static boolean isLayerValid(Layer l) {
1223        if (l == null)
1224            return false;
1225
1226        return MainApplication.getLayerManager().containsLayer(l);
1227    }
1228
1229    /**
1230     * Returns info about layer.
1231     * @param l the layer
1232     * @return info about layer {@code l}
1233     */
1234    public static MultikeyInfo getLayerInfo(Layer l) {
1235        if (l == null)
1236            return null;
1237
1238        int index = MainApplication.getLayerManager().getLayers().indexOf(l);
1239        if (index < 0)
1240            return null;
1241
1242        return new MultikeyInfo(index, l.getName());
1243    }
1244
1245    @Override
1246    public void displaySettingsChanged(DisplaySettingsChangeEvent e) {
1247        if ("displacement".equals(e.getChangedSetting())) {
1248            layerList.repaint();
1249        }
1250    }
1251}