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