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