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