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