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.setValueIsAdjusting(true);
844            selectionModel.clearSelection();
845            for (int row : sel) {
846                selectionModel.addSelectionInterval(row-1, row-1);
847            }
848            selectionModel.setValueIsAdjusting(false);
849            ensureSelectedIsVisible();
850        }
851
852        /**
853         * Replies true if the currently selected layers can move down by one position
854         *
855         * @return true if the currently selected layers can move down by one position
856         */
857        public boolean canMoveDown() {
858            List<Integer> sel = getSelectedRows();
859            return !sel.isEmpty() && sel.get(sel.size()-1) < getLayers().size()-1;
860        }
861
862        /**
863         * Move down the currently selected layers by one position
864         */
865        public void moveDown() {
866            if (!canMoveDown())
867                return;
868            List<Integer> sel = getSelectedRows();
869            Collections.reverse(sel);
870            List<Layer> layers = getLayers();
871            for (int row : sel) {
872                Layer l1 = layers.get(row);
873                Layer l2 = layers.get(row+1);
874                Main.map.mapView.moveLayer(l1, row+1);
875                Main.map.mapView.moveLayer(l2, row);
876            }
877            fireTableDataChanged();
878            selectionModel.setValueIsAdjusting(true);
879            selectionModel.clearSelection();
880            for (int row : sel) {
881                selectionModel.addSelectionInterval(row+1, row+1);
882            }
883            selectionModel.setValueIsAdjusting(false);
884            ensureSelectedIsVisible();
885        }
886
887        /**
888         * Make sure the first of the selected layers is visible in the views of this model.
889         */
890        private void ensureSelectedIsVisible() {
891            int index = selectionModel.getMinSelectionIndex();
892            if (index < 0)
893                return;
894            List<Layer> layers = getLayers();
895            if (index >= layers.size())
896                return;
897            Layer layer = layers.get(index);
898            fireMakeVisible(index, layer);
899        }
900
901        /**
902         * Replies a list of layers which are possible merge targets for <code>source</code>
903         *
904         * @param source the source layer
905         * @return a list of layers which are possible merge targets
906         * for <code>source</code>. Never null, but can be empty.
907         */
908        public List<Layer> getPossibleMergeTargets(Layer source) {
909            List<Layer> targets = new ArrayList<>();
910            if (source == null) {
911                return targets;
912            }
913            for (Layer target : getLayers()) {
914                if (source == target) {
915                    continue;
916                }
917                if (target.isMergable(source) && source.isMergable(target)) {
918                    targets.add(target);
919                }
920            }
921            return targets;
922        }
923
924        /**
925         * Replies the list of layers currently managed by {@link MapView}.
926         * Never null, but can be empty.
927         *
928         * @return the list of layers currently managed by {@link MapView}.
929         * Never null, but can be empty.
930         */
931        public List<Layer> getLayers() {
932            return getLayerManager().getLayers();
933        }
934
935        /**
936         * Ensures that at least one layer is selected in the layer dialog
937         *
938         */
939        private void ensureActiveSelected() {
940            List<Layer> layers = getLayers();
941            if (layers.isEmpty())
942                return;
943            final Layer activeLayer = getActiveLayer();
944            if (activeLayer != null) {
945                // there's an active layer - select it and make it visible
946                int idx = layers.indexOf(activeLayer);
947                selectionModel.setSelectionInterval(idx, idx);
948                ensureSelectedIsVisible();
949            } else {
950                // no active layer - select the first one and make it visible
951                selectionModel.setSelectionInterval(0, 0);
952                ensureSelectedIsVisible();
953            }
954        }
955
956        /**
957         * Replies the active layer. null, if no active layer is available
958         *
959         * @return the active layer. null, if no active layer is available
960         */
961        private Layer getActiveLayer() {
962            return getLayerManager().getActiveLayer();
963        }
964
965        /* ------------------------------------------------------------------------------ */
966        /* Interface TableModel                                                           */
967        /* ------------------------------------------------------------------------------ */
968
969        @Override
970        public int getRowCount() {
971            List<Layer> layers = getLayers();
972            return layers == null ? 0 : layers.size();
973        }
974
975        @Override
976        public int getColumnCount() {
977            return 4;
978        }
979
980        @Override
981        public Object getValueAt(int row, int col) {
982            List<Layer> layers = getLayers();
983            if (row >= 0 && row < layers.size()) {
984                switch (col) {
985                case 0: return layers.get(row) == getActiveLayer();
986                case 1:
987                case 2:
988                case 3: return layers.get(row);
989                default: // Do nothing
990                }
991            }
992            return null;
993        }
994
995        @Override
996        public boolean isCellEditable(int row, int col) {
997            if (col == 0 && getActiveLayer() == getLayers().get(row))
998                return false;
999            return true;
1000        }
1001
1002        @Override
1003        public void setValueAt(Object value, int row, int col) {
1004            List<Layer> layers = getLayers();
1005            if (row < layers.size()) {
1006                Layer l = layers.get(row);
1007                switch (col) {
1008                case 0:
1009                    getLayerManager().setActiveLayer(l);
1010                    l.setVisible(true);
1011                    break;
1012                case 1:
1013                    NativeScaleLayer oldLayer = Main.map.mapView.getNativeScaleLayer();
1014                    if (oldLayer == l) {
1015                        Main.map.mapView.setNativeScaleLayer(null);
1016                    } else if (l instanceof NativeScaleLayer) {
1017                        Main.map.mapView.setNativeScaleLayer((NativeScaleLayer) l);
1018                        if (oldLayer != null) {
1019                            int idx = getLayers().indexOf(oldLayer);
1020                            if (idx >= 0) {
1021                                fireTableCellUpdated(idx, col);
1022                            }
1023                        }
1024                    }
1025                    break;
1026                case 2:
1027                    l.setVisible((Boolean) value);
1028                    break;
1029                case 3:
1030                    l.rename((String) value);
1031                    break;
1032                default:
1033                    throw new IllegalArgumentException("Wrong column: " + col);
1034                }
1035                fireTableCellUpdated(row, col);
1036            }
1037        }
1038
1039        /* ------------------------------------------------------------------------------ */
1040        /* Interface ActiveLayerChangeListener                                            */
1041        /* ------------------------------------------------------------------------------ */
1042        @Override
1043        public void activeOrEditLayerChanged(ActiveLayerChangeEvent e) {
1044            Layer oldLayer = e.getPreviousActiveLayer();
1045            if (oldLayer != null) {
1046                int idx = getLayers().indexOf(oldLayer);
1047                if (idx >= 0) {
1048                    fireTableRowsUpdated(idx, idx);
1049                }
1050            }
1051
1052            Layer newLayer = getActiveLayer();
1053            if (newLayer != null) {
1054                int idx = getLayers().indexOf(newLayer);
1055                if (idx >= 0) {
1056                    fireTableRowsUpdated(idx, idx);
1057                }
1058            }
1059            ensureActiveSelected();
1060        }
1061
1062        /* ------------------------------------------------------------------------------ */
1063        /* Interface LayerChangeListener                                                  */
1064        /* ------------------------------------------------------------------------------ */
1065        @Override
1066        public void layerAdded(LayerAddEvent e) {
1067            onAddLayer(e.getAddedLayer());
1068        }
1069
1070        @Override
1071        public void layerRemoving(LayerRemoveEvent e) {
1072            onRemoveLayer(e.getRemovedLayer());
1073        }
1074
1075        @Override
1076        public void layerOrderChanged(LayerOrderChangeEvent e) {
1077            fireTableDataChanged();
1078        }
1079
1080        /* ------------------------------------------------------------------------------ */
1081        /* Interface PropertyChangeListener                                               */
1082        /* ------------------------------------------------------------------------------ */
1083        @Override
1084        public void propertyChange(PropertyChangeEvent evt) {
1085            if (evt.getSource() instanceof Layer) {
1086                Layer layer = (Layer) evt.getSource();
1087                final int idx = getLayers().indexOf(layer);
1088                if (idx < 0)
1089                    return;
1090                fireRefresh();
1091            }
1092        }
1093    }
1094
1095    /**
1096     * This component displays a list of layers and provides the methods needed by {@link LayerListModel}.
1097     */
1098    static class LayerList extends JTable {
1099
1100        LayerList(LayerListModel dataModel) {
1101            super(dataModel);
1102            dataModel.setLayerList(this);
1103            if (!GraphicsEnvironment.isHeadless()) {
1104                setDragEnabled(true);
1105            }
1106            setDropMode(DropMode.INSERT_ROWS);
1107            setTransferHandler(new LayerListTransferHandler());
1108        }
1109
1110        public void scrollToVisible(int row, int col) {
1111            if (!(getParent() instanceof JViewport))
1112                return;
1113            JViewport viewport = (JViewport) getParent();
1114            Rectangle rect = getCellRect(row, col, true);
1115            Point pt = viewport.getViewPosition();
1116            rect.setLocation(rect.x - pt.x, rect.y - pt.y);
1117            viewport.scrollRectToVisible(rect);
1118        }
1119
1120        @Override
1121        public LayerListModel getModel() {
1122            return (LayerListModel) super.getModel();
1123        }
1124    }
1125
1126    /**
1127     * Creates a {@link ShowHideLayerAction} in the context of this {@link LayerListDialog}.
1128     *
1129     * @return the action
1130     */
1131    public ShowHideLayerAction createShowHideLayerAction() {
1132        return new ShowHideLayerAction(model);
1133    }
1134
1135    /**
1136     * Creates a {@link DeleteLayerAction} in the context of this {@link LayerListDialog}.
1137     *
1138     * @return the action
1139     */
1140    public DeleteLayerAction createDeleteLayerAction() {
1141        return new DeleteLayerAction(model);
1142    }
1143
1144    /**
1145     * Creates a {@link ActivateLayerAction} for <code>layer</code> in the context of this {@link LayerListDialog}.
1146     *
1147     * @param layer the layer
1148     * @return the action
1149     */
1150    public ActivateLayerAction createActivateLayerAction(Layer layer) {
1151        return new ActivateLayerAction(layer, model);
1152    }
1153
1154    /**
1155     * Creates a {@link MergeLayerAction} for <code>layer</code> in the context of this {@link LayerListDialog}.
1156     *
1157     * @param layer the layer
1158     * @return the action
1159     */
1160    public MergeAction createMergeLayerAction(Layer layer) {
1161        return new MergeAction(layer, model);
1162    }
1163
1164    /**
1165     * Creates a {@link DuplicateAction} for <code>layer</code> in the context of this {@link LayerListDialog}.
1166     *
1167     * @param layer the layer
1168     * @return the action
1169     */
1170    public DuplicateAction createDuplicateLayerAction(Layer layer) {
1171        return new DuplicateAction(layer, model);
1172    }
1173
1174    /**
1175     * Returns the layer at given index, or {@code null}.
1176     * @param index the index
1177     * @return the layer at given index, or {@code null} if index out of range
1178     */
1179    public static Layer getLayerForIndex(int index) {
1180        List<Layer> layers = Main.getLayerManager().getLayers();
1181
1182        if (index < layers.size() && index >= 0)
1183            return layers.get(index);
1184        else
1185            return null;
1186    }
1187
1188    /**
1189     * Returns a list of info on all layers of a given class.
1190     * @param layerClass The layer class. This is not {@code Class<? extends Layer>} on purpose,
1191     *                   to allow asking for layers implementing some interface
1192     * @return list of info on all layers assignable from {@code layerClass}
1193     */
1194    public static List<MultikeyInfo> getLayerInfoByClass(Class<?> layerClass) {
1195        List<MultikeyInfo> result = new ArrayList<>();
1196
1197        List<Layer> layers = Main.getLayerManager().getLayers();
1198
1199        int index = 0;
1200        for (Layer l: layers) {
1201            if (layerClass.isAssignableFrom(l.getClass())) {
1202                result.add(new MultikeyInfo(index, l.getName()));
1203            }
1204            index++;
1205        }
1206
1207        return result;
1208    }
1209
1210    /**
1211     * Determines if a layer is valid (contained in global layer list).
1212     * @param l the layer
1213     * @return {@code true} if layer {@code l} is contained in current layer list
1214     */
1215    public static boolean isLayerValid(Layer l) {
1216        if (l == null)
1217            return false;
1218
1219        return Main.getLayerManager().containsLayer(l);
1220    }
1221
1222    /**
1223     * Returns info about layer.
1224     * @param l the layer
1225     * @return info about layer {@code l}
1226     */
1227    public static MultikeyInfo getLayerInfo(Layer l) {
1228        if (l == null)
1229            return null;
1230
1231        int index = Main.getLayerManager().getLayers().indexOf(l);
1232        if (index < 0)
1233            return null;
1234
1235        return new MultikeyInfo(index, l.getName());
1236    }
1237}