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