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