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