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