001    // License: GPL. For details, see LICENSE file.
002    package org.openstreetmap.josm.gui.dialogs;
003    
004    import static org.openstreetmap.josm.tools.I18n.tr;
005    
006    import java.awt.Color;
007    import java.awt.Component;
008    import java.awt.Dimension;
009    import java.awt.Font;
010    import java.awt.Point;
011    import java.awt.Rectangle;
012    import java.awt.event.ActionEvent;
013    import java.awt.event.InputEvent;
014    import java.awt.event.KeyEvent;
015    import java.awt.event.MouseEvent;
016    import java.beans.PropertyChangeEvent;
017    import java.beans.PropertyChangeListener;
018    import java.lang.ref.WeakReference;
019    import java.util.ArrayList;
020    import java.util.Arrays;
021    import java.util.Collections;
022    import java.util.List;
023    import java.util.concurrent.CopyOnWriteArrayList;
024    
025    import javax.swing.AbstractAction;
026    import javax.swing.Action;
027    import javax.swing.DefaultCellEditor;
028    import javax.swing.DefaultListSelectionModel;
029    import javax.swing.ImageIcon;
030    import javax.swing.JCheckBox;
031    import javax.swing.JComponent;
032    import javax.swing.JLabel;
033    import javax.swing.JMenuItem;
034    import javax.swing.JPopupMenu;
035    import javax.swing.JSlider;
036    import javax.swing.JTable;
037    import javax.swing.JTextField;
038    import javax.swing.JViewport;
039    import javax.swing.KeyStroke;
040    import javax.swing.ListSelectionModel;
041    import javax.swing.UIManager;
042    import javax.swing.event.ChangeEvent;
043    import javax.swing.event.ChangeListener;
044    import javax.swing.event.ListDataEvent;
045    import javax.swing.event.ListSelectionEvent;
046    import javax.swing.event.ListSelectionListener;
047    import javax.swing.event.TableModelEvent;
048    import javax.swing.event.TableModelListener;
049    import javax.swing.table.AbstractTableModel;
050    import javax.swing.table.DefaultTableCellRenderer;
051    import javax.swing.table.TableCellRenderer;
052    import javax.swing.table.TableModel;
053    
054    import org.openstreetmap.josm.Main;
055    import org.openstreetmap.josm.actions.MergeLayerAction;
056    import org.openstreetmap.josm.gui.MapFrame;
057    import org.openstreetmap.josm.gui.MapView;
058    import org.openstreetmap.josm.gui.SideButton;
059    import org.openstreetmap.josm.gui.help.HelpUtil;
060    import org.openstreetmap.josm.gui.io.SaveLayersDialog;
061    import org.openstreetmap.josm.gui.layer.JumpToMarkerActions;
062    import org.openstreetmap.josm.gui.layer.Layer;
063    import org.openstreetmap.josm.gui.layer.Layer.LayerAction;
064    import org.openstreetmap.josm.gui.layer.OsmDataLayer;
065    import org.openstreetmap.josm.gui.util.GuiHelper;
066    import org.openstreetmap.josm.gui.widgets.PopupMenuLauncher;
067    import org.openstreetmap.josm.tools.CheckParameterUtil;
068    import org.openstreetmap.josm.tools.ImageProvider;
069    import org.openstreetmap.josm.tools.InputMapUtils;
070    import org.openstreetmap.josm.tools.MultikeyActionsHandler;
071    import org.openstreetmap.josm.tools.MultikeyShortcutAction;
072    import org.openstreetmap.josm.tools.MultikeyShortcutAction.MultikeyInfo;
073    import org.openstreetmap.josm.tools.Shortcut;
074    
075    /**
076     * This is a toggle dialog which displays the list of layers. Actions allow to
077     * change the ordering of the layers, to hide/show layers, to activate layers,
078     * and to delete layers.
079     *
080     */
081    public class LayerListDialog extends ToggleDialog {
082        /** the unique instance of the dialog */
083        static private LayerListDialog instance;
084    
085        /**
086         * Creates the instance of the dialog. It's connected to the map frame <code>mapFrame</code>
087         *
088         * @param mapFrame the map frame
089         */
090        static public void createInstance(MapFrame mapFrame) {
091            if (instance != null)
092                throw new IllegalStateException("Dialog was already created");
093            instance = new LayerListDialog(mapFrame);
094    
095        }
096    
097        /**
098         * Replies the instance of the dialog
099         *
100         * @return the instance of the dialog
101         * @throws IllegalStateException thrown, if the dialog is not created yet
102         * @see #createInstance(MapFrame)
103         */
104        static public LayerListDialog getInstance() throws IllegalStateException {
105            if (instance == null)
106                throw new IllegalStateException("Dialog not created yet. Invoke createInstance() first");
107            return instance;
108        }
109    
110        /** the model for the layer list */
111        private LayerListModel model;
112    
113        /** the selection model */
114        private DefaultListSelectionModel selectionModel;
115    
116        /** the list of layers (technically its a JTable, but appears like a list) */
117        private LayerList layerList;
118    
119        private SideButton opacityButton;
120    
121        ActivateLayerAction activateLayerAction;
122        ShowHideLayerAction showHideLayerAction;
123    
124        //TODO This duplicates ShowHide actions functionality
125        /** stores which layer index to toggle and executes the ShowHide action if the layer is present */
126        private final class ToggleLayerIndexVisibility extends AbstractAction {
127            int layerIndex = -1;
128            public ToggleLayerIndexVisibility(int layerIndex) {
129                this.layerIndex = layerIndex;
130            }
131            @Override
132            public void actionPerformed(ActionEvent e) {
133                final Layer l = model.getLayer(model.getRowCount() - layerIndex - 1);
134                if(l != null) {
135                    l.toggleVisible();
136                }
137            }
138        }
139    
140        private final Shortcut[] visibilityToggleShortcuts = new Shortcut[10];
141        private final ToggleLayerIndexVisibility[] visibilityToggleActions = new ToggleLayerIndexVisibility[10];
142        /**
143         * registers (shortcut to toggle right hand side toggle dialogs)+(number keys) shortcuts
144         * to toggle the visibility of the first ten layers.
145         */
146        private final void createVisibilityToggleShortcuts() {
147            final int[] k = { KeyEvent.VK_1, KeyEvent.VK_2, KeyEvent.VK_3, KeyEvent.VK_4,
148                    KeyEvent.VK_5, KeyEvent.VK_6, KeyEvent.VK_7, KeyEvent.VK_8,
149                    KeyEvent.VK_9, KeyEvent.VK_0 };
150    
151            for(int i=0; i < 10; i++) {
152                visibilityToggleShortcuts[i] = Shortcut.registerShortcut("subwindow:layers:toggleLayer" + (i+1),
153                        tr("Toggle visibility of layer: {0}", (i+1)), k[i], Shortcut.ALT);
154                visibilityToggleActions[i] = new ToggleLayerIndexVisibility(i);
155                Main.registerActionShortcut(visibilityToggleActions[i], visibilityToggleShortcuts[i]);
156            }
157        }
158    
159        /**
160         * Create an layer list and attach it to the given mapView.
161         */
162        protected LayerListDialog(MapFrame mapFrame) {
163            super(tr("Layers"), "layerlist", tr("Open a list of all loaded layers."),
164                    Shortcut.registerShortcut("subwindow:layers", tr("Toggle: {0}", tr("Layers")), KeyEvent.VK_L,
165                            Shortcut.ALT_SHIFT), 100, true);
166    
167            // create the models
168            //
169            selectionModel = new DefaultListSelectionModel();
170            selectionModel.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
171            model = new LayerListModel(selectionModel);
172    
173            // create the list control
174            //
175            layerList = new LayerList(model);
176            layerList.setSelectionModel(selectionModel);
177            layerList.addMouseListener(new PopupMenuHandler());
178            layerList.setBackground(UIManager.getColor("Button.background"));
179            layerList.putClientProperty("terminateEditOnFocusLost", true);
180            layerList.putClientProperty("JTable.autoStartsEdit", false);
181            layerList.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
182            layerList.setTableHeader(null);
183            layerList.setShowGrid(false);
184            layerList.setIntercellSpacing(new Dimension(0, 0));
185            layerList.getColumnModel().getColumn(0).setCellRenderer(new ActiveLayerCellRenderer());
186            layerList.getColumnModel().getColumn(0).setCellEditor(new DefaultCellEditor(new ActiveLayerCheckBox()));
187            layerList.getColumnModel().getColumn(0).setMaxWidth(12);
188            layerList.getColumnModel().getColumn(0).setPreferredWidth(12);
189            layerList.getColumnModel().getColumn(0).setResizable(false);
190            layerList.getColumnModel().getColumn(1).setCellRenderer(new LayerVisibleCellRenderer());
191            layerList.getColumnModel().getColumn(1).setCellEditor(new LayerVisibleCellEditor(new LayerVisibleCheckBox()));
192            layerList.getColumnModel().getColumn(1).setMaxWidth(16);
193            layerList.getColumnModel().getColumn(1).setPreferredWidth(16);
194            layerList.getColumnModel().getColumn(1).setResizable(false);
195            layerList.getColumnModel().getColumn(2).setCellRenderer(new LayerNameCellRenderer());
196            layerList.getColumnModel().getColumn(2).setCellEditor(new LayerNameCellEditor(new JTextField()));
197            for (KeyStroke ks : new KeyStroke[] {
198                    KeyStroke.getKeyStroke(KeyEvent.VK_C, InputEvent.CTRL_MASK),
199                    KeyStroke.getKeyStroke(KeyEvent.VK_V, InputEvent.CTRL_MASK),
200                    KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, InputEvent.SHIFT_MASK),
201                    KeyStroke.getKeyStroke(KeyEvent.VK_UP, InputEvent.SHIFT_MASK),
202                    KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, InputEvent.SHIFT_MASK),
203                    KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, InputEvent.SHIFT_MASK),
204                    KeyStroke.getKeyStroke(KeyEvent.VK_PAGE_UP, 0),
205                    KeyStroke.getKeyStroke(KeyEvent.VK_PAGE_DOWN, 0),
206            })
207            {
208                layerList.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(ks, new Object());
209            }
210    
211            // init the model
212            //
213            final MapView mapView = mapFrame.mapView;
214            model.populate();
215            model.setSelectedLayer(mapView.getActiveLayer());
216            model.addLayerListModelListener(
217                    new LayerListModelListener() {
218                        @Override
219                        public void makeVisible(int row, Layer layer) {
220                            layerList.scrollToVisible(row, 0);
221                            layerList.repaint();
222                        }
223                        @Override
224                        public void refresh() {
225                            layerList.repaint();
226                        }
227                    }
228                    );
229    
230            // -- move up action
231            MoveUpAction moveUpAction = new MoveUpAction();
232            adaptTo(moveUpAction, model);
233            adaptTo(moveUpAction,selectionModel);
234    
235            // -- move down action
236            MoveDownAction moveDownAction = new MoveDownAction();
237            adaptTo(moveDownAction, model);
238            adaptTo(moveDownAction,selectionModel);
239    
240            // -- activate action
241            activateLayerAction = new ActivateLayerAction();
242            activateLayerAction.updateEnabledState();
243            MultikeyActionsHandler.getInstance().addAction(activateLayerAction);
244            adaptTo(activateLayerAction, selectionModel);
245    
246            JumpToMarkerActions.initialize();
247    
248            // -- show hide action
249            showHideLayerAction = new ShowHideLayerAction();
250            MultikeyActionsHandler.getInstance().addAction(showHideLayerAction);
251            adaptTo(showHideLayerAction, selectionModel);
252    
253            //-- layer opacity action
254            LayerOpacityAction layerOpacityAction = new LayerOpacityAction();
255            adaptTo(layerOpacityAction, selectionModel);
256            opacityButton = new SideButton(layerOpacityAction, false);
257    
258            // -- merge layer action
259            MergeAction mergeLayerAction = new MergeAction();
260            adaptTo(mergeLayerAction, model);
261            adaptTo(mergeLayerAction,selectionModel);
262    
263            // -- duplicate layer action
264            DuplicateAction duplicateLayerAction = new DuplicateAction();
265            adaptTo(duplicateLayerAction, model);
266            adaptTo(duplicateLayerAction, selectionModel);
267    
268            //-- delete layer action
269            DeleteLayerAction deleteLayerAction = new DeleteLayerAction();
270            layerList.getActionMap().put("deleteLayer", deleteLayerAction);
271            adaptTo(deleteLayerAction, selectionModel);
272            getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(
273                    KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0),"delete"
274                    );
275            getActionMap().put("delete", deleteLayerAction);
276    
277            // Activate layer on Enter key press
278            InputMapUtils.addEnterAction(layerList, new AbstractAction() {
279                public void actionPerformed(ActionEvent e) {
280                    activateLayerAction.actionPerformed(null);
281                    layerList.requestFocus();
282                }
283            });
284    
285            // Show/Activate layer on Enter key press
286            InputMapUtils.addSpacebarAction(layerList, showHideLayerAction);
287    
288            createLayout(layerList, true, Arrays.asList(new SideButton[] {
289                    new SideButton(moveUpAction, false),
290                    new SideButton(moveDownAction, false),
291                    new SideButton(activateLayerAction, false),
292                    new SideButton(showHideLayerAction, false),
293                    opacityButton,
294                    new SideButton(mergeLayerAction, false),
295                    new SideButton(duplicateLayerAction, false),
296                    new SideButton(deleteLayerAction, false)
297            }));
298    
299            createVisibilityToggleShortcuts();
300        }
301    
302        @Override
303        public void showNotify() {
304            MapView.addLayerChangeListener(activateLayerAction);
305            MapView.addLayerChangeListener(model);
306            model.populate();
307        }
308    
309        @Override
310        public void hideNotify() {
311            MapView.removeLayerChangeListener(model);
312            MapView.removeLayerChangeListener(activateLayerAction);
313        }
314    
315        public LayerListModel getModel() {
316            return model;
317        }
318    
319        protected interface IEnabledStateUpdating {
320            void updateEnabledState();
321        }
322    
323        /**
324         * Wires <code>listener</code> to <code>listSelectionModel</code> in such a way, that
325         * <code>listener</code> receives a {@link IEnabledStateUpdating#updateEnabledState()}
326         * on every {@link ListSelectionEvent}.
327         *
328         * @param listener  the listener
329         * @param listSelectionModel  the source emitting {@link ListSelectionEvent}s
330         */
331        protected void adaptTo(final IEnabledStateUpdating listener, ListSelectionModel listSelectionModel) {
332            listSelectionModel.addListSelectionListener(
333                    new ListSelectionListener() {
334                        @Override
335                        public void valueChanged(ListSelectionEvent e) {
336                            listener.updateEnabledState();
337                        }
338                    }
339                    );
340        }
341    
342        /**
343         * Wires <code>listener</code> to <code>listModel</code> in such a way, that
344         * <code>listener</code> receives a {@link IEnabledStateUpdating#updateEnabledState()}
345         * on every {@link ListDataEvent}.
346         *
347         * @param listener  the listener
348         * @param listSelectionModel  the source emitting {@link ListDataEvent}s
349         */
350        protected void adaptTo(final IEnabledStateUpdating listener, LayerListModel listModel) {
351            listModel.addTableModelListener(
352                    new TableModelListener() {
353    
354                        @Override
355                        public void tableChanged(TableModelEvent e) {
356                            listener.updateEnabledState();
357                        }
358                    }
359                    );
360        }
361    
362        @Override
363        public void destroy() {
364            for(int i=0; i < 10; i++) {
365                Main.unregisterActionShortcut(visibilityToggleActions[i], visibilityToggleShortcuts[i]);
366            }
367            MultikeyActionsHandler.getInstance().removeAction(activateLayerAction);
368            MultikeyActionsHandler.getInstance().removeAction(showHideLayerAction);
369            JumpToMarkerActions.unregisterActions();
370            super.destroy();
371            instance = null;
372        }
373    
374        /**
375         * The action to delete the currently selected layer
376         */
377        public final class DeleteLayerAction extends AbstractAction implements IEnabledStateUpdating, LayerAction {
378            /**
379             * Creates a {@link DeleteLayerAction} which will delete the currently
380             * selected layers in the layer dialog.
381             *
382             */
383            public DeleteLayerAction() {
384                putValue(SMALL_ICON,ImageProvider.get("dialogs", "delete"));
385                putValue(SHORT_DESCRIPTION, tr("Delete the selected layers."));
386                putValue(NAME, tr("Delete"));
387                putValue("help", HelpUtil.ht("/Dialog/LayerList#DeleteLayer"));
388                updateEnabledState();
389            }
390    
391            @Override
392            public void actionPerformed(ActionEvent e) {
393                List<Layer> selectedLayers = getModel().getSelectedLayers();
394                if (selectedLayers.isEmpty())
395                    return;
396                if (!Main.saveUnsavedModifications(selectedLayers, false))
397                    return;
398                for (Layer l: selectedLayers) {
399                    Main.main.removeLayer(l);
400                }
401            }
402    
403            @Override
404            public void updateEnabledState() {
405                setEnabled(! getModel().getSelectedLayers().isEmpty());
406            }
407    
408            @Override
409            public Component createMenuComponent() {
410                return new JMenuItem(this);
411            }
412    
413            @Override
414            public boolean supportLayers(List<Layer> layers) {
415                return true;
416            }
417    
418            @Override
419            public boolean equals(Object obj) {
420                return obj instanceof DeleteLayerAction;
421            }
422    
423            @Override
424            public int hashCode() {
425                return getClass().hashCode();
426            }
427        }
428    
429        public final class ShowHideLayerAction extends AbstractAction implements IEnabledStateUpdating, LayerAction, MultikeyShortcutAction {
430    
431            private WeakReference<Layer> lastLayer;
432            private Shortcut multikeyShortcut;
433    
434            /**
435             * Creates a {@link ShowHideLayerAction} which will toggle the visibility of
436             * the currently selected layers
437             *
438             */
439            public ShowHideLayerAction(boolean init) {
440                putValue(NAME, tr("Show/hide"));
441                putValue(SMALL_ICON, ImageProvider.get("dialogs", "showhide"));
442                putValue(SHORT_DESCRIPTION, tr("Toggle visible state of the selected layer."));
443                putValue("help", HelpUtil.ht("/Dialog/LayerList#ShowHideLayer"));
444                multikeyShortcut = Shortcut.registerShortcut("core_multikey:showHideLayer", tr("Multikey: {0}",
445                        tr("Show/hide layer")), KeyEvent.VK_S, Shortcut.SHIFT);
446                multikeyShortcut.setAccelerator(this);
447                if (init) {
448                    updateEnabledState();
449                }
450            }
451    
452            public ShowHideLayerAction() {
453                this(true);
454            }
455    
456            @Override
457            public Shortcut getMultikeyShortcut() {
458                return multikeyShortcut;
459            }
460    
461            @Override
462            public void actionPerformed(ActionEvent e) {
463                for(Layer l : model.getSelectedLayers()) {
464                    l.toggleVisible();
465                }
466            }
467    
468            @Override
469            public void executeMultikeyAction(int index, boolean repeat) {
470                Layer l = LayerListDialog.getLayerForIndex(index);
471                if (l != null) {
472                    l.toggleVisible();
473                    lastLayer = new WeakReference<Layer>(l);
474                } else if (repeat && lastLayer != null) {
475                    l = lastLayer.get();
476                    if (LayerListDialog.isLayerValid(l)) {
477                        l.toggleVisible();
478                    }
479                }
480            }
481    
482            @Override
483            public void updateEnabledState() {
484                setEnabled(!model.getSelectedLayers().isEmpty());
485            }
486    
487            @Override
488            public Component createMenuComponent() {
489                return new JMenuItem(this);
490            }
491    
492            @Override
493            public boolean supportLayers(List<Layer> layers) {
494                return true;
495            }
496    
497            @Override
498            public boolean equals(Object obj) {
499                return obj instanceof ShowHideLayerAction;
500            }
501    
502            @Override
503            public int hashCode() {
504                return getClass().hashCode();
505            }
506    
507            @Override
508            public List<MultikeyInfo> getMultikeyCombinations() {
509                return LayerListDialog.getLayerInfoByClass(Layer.class);
510            }
511    
512            @Override
513            public MultikeyInfo getLastMultikeyAction() {
514                if (lastLayer != null)
515                    return LayerListDialog.getLayerInfo(lastLayer.get());
516                return null;
517            }
518        }
519    
520        public final class LayerOpacityAction extends AbstractAction implements IEnabledStateUpdating, LayerAction {
521            private Layer layer;
522            private JPopupMenu popup;
523            private JSlider slider = new JSlider(JSlider.VERTICAL);
524    
525            /**
526             * Creates a {@link LayerOpacityAction} which allows to chenge the
527             * opacity of one or more layers.
528             *
529             * @param layer  the layer. Must not be null.
530             * @exception IllegalArgumentException thrown, if layer is null
531             */
532            public LayerOpacityAction(Layer layer) throws IllegalArgumentException {
533                this();
534                putValue(NAME, tr("Opacity"));
535                CheckParameterUtil.ensureParameterNotNull(layer, "layer");
536                this.layer = layer;
537                updateEnabledState();
538            }
539    
540            /**
541             * Creates a {@link ShowHideLayerAction} which will toggle the visibility of
542             * the currently selected layers
543             *
544             */
545            public LayerOpacityAction() {
546                putValue(NAME, tr("Opacity"));
547                putValue(SHORT_DESCRIPTION, tr("Adjust opacity of the layer."));
548                putValue(SMALL_ICON, ImageProvider.get("dialogs/layerlist", "transparency"));
549                updateEnabledState();
550    
551                popup = new JPopupMenu();
552                slider.addChangeListener(new ChangeListener() {
553                    @Override
554                    public void stateChanged(ChangeEvent e) {
555                        setOpacity((double)slider.getValue()/100);
556                    }
557                });
558                popup.add(slider);
559            }
560    
561            private void setOpacity(double value) {
562                if (!isEnabled()) return;
563                if (layer != null) {
564                    layer.setOpacity(value);
565                } else {
566                    for(Layer layer: model.getSelectedLayers()) {
567                        layer.setOpacity(value);
568                    }
569                }
570            }
571    
572            private double getOpacity() {
573                if (layer != null)
574                    return layer.getOpacity();
575                else {
576                    double opacity = 0;
577                    List<Layer> layers = model.getSelectedLayers();
578                    for(Layer layer: layers) {
579                        opacity += layer.getOpacity();
580                    }
581                    return opacity / layers.size();
582                }
583            }
584    
585            @Override
586            public void actionPerformed(ActionEvent e) {
587                slider.setValue((int)Math.round(getOpacity()*100));
588                if (e.getSource() == opacityButton) {
589                    popup.show(opacityButton, 0, opacityButton.getHeight());
590                } else {
591                    // Action can be trigger either by opacity button or by popup menu (in case toggle buttons are hidden).
592                    // In that case, show it in the middle of screen (because opacityButton is not visible)
593                    popup.show(Main.parent, Main.parent.getWidth() / 2, (Main.parent.getHeight() - popup.getHeight()) / 2);
594                }
595            }
596    
597            @Override
598            public void updateEnabledState() {
599                if (layer == null) {
600                    setEnabled(! getModel().getSelectedLayers().isEmpty());
601                } else {
602                    setEnabled(true);
603                }
604            }
605    
606            @Override
607            public Component createMenuComponent() {
608                return new JMenuItem(this);
609            }
610    
611            @Override
612            public boolean supportLayers(List<Layer> layers) {
613                return true;
614            }
615    
616            @Override
617            public boolean equals(Object obj) {
618                return obj instanceof LayerOpacityAction;
619            }
620    
621            @Override
622            public int hashCode() {
623                return getClass().hashCode();
624            }
625        }
626    
627        /**
628         * The action to activate the currently selected layer
629         */
630    
631        public final class ActivateLayerAction extends AbstractAction implements IEnabledStateUpdating, MapView.LayerChangeListener, MultikeyShortcutAction{
632            private  Layer layer;
633            private Shortcut multikeyShortcut;
634    
635            public ActivateLayerAction(Layer layer) {
636                this();
637                CheckParameterUtil.ensureParameterNotNull(layer, "layer");
638                this.layer = layer;
639                putValue(NAME, tr("Activate"));
640                updateEnabledState();
641            }
642    
643            public ActivateLayerAction() {
644                putValue(NAME, tr("Activate"));
645                putValue(SMALL_ICON, ImageProvider.get("dialogs", "activate"));
646                putValue(SHORT_DESCRIPTION, tr("Activate the selected layer"));
647                multikeyShortcut = Shortcut.registerShortcut("core_multikey:activateLayer", tr("Multikey: {0}",
648                        tr("Activate layer")), KeyEvent.VK_A, Shortcut.SHIFT);
649                multikeyShortcut.setAccelerator(this);
650                putValue("help", HelpUtil.ht("/Dialog/LayerList#ActivateLayer"));
651            }
652    
653            @Override
654            public Shortcut getMultikeyShortcut() {
655                return multikeyShortcut;
656            }
657    
658            @Override
659            public void actionPerformed(ActionEvent e) {
660                Layer toActivate;
661                if (layer != null) {
662                    toActivate = layer;
663                } else {
664                    toActivate = model.getSelectedLayers().get(0);
665                }
666                execute(toActivate);
667            }
668    
669            private void execute(Layer layer) {
670                // model is  going to be updated via LayerChangeListener
671                // and PropertyChangeEvents
672                Main.map.mapView.setActiveLayer(layer);
673                layer.setVisible(true);
674            }
675    
676            protected boolean isActiveLayer(Layer layer) {
677                if (Main.map == null) return false;
678                if (Main.map.mapView == null) return false;
679                return Main.map.mapView.getActiveLayer() == layer;
680            }
681    
682            @Override
683            public void updateEnabledState() {
684                GuiHelper.runInEDTAndWait(new Runnable() {
685                    @Override
686                    public void run() {
687                        if (layer == null) {
688                            if (getModel().getSelectedLayers().size() != 1) {
689                                setEnabled(false);
690                                return;
691                            }
692                            Layer selectedLayer = getModel().getSelectedLayers().get(0);
693                            setEnabled(!isActiveLayer(selectedLayer));
694                        } else {
695                            setEnabled(!isActiveLayer(layer));
696                        }
697                    }
698                });
699            }
700    
701            @Override
702            public void activeLayerChange(Layer oldLayer, Layer newLayer) {
703                updateEnabledState();
704            }
705            @Override
706            public void layerAdded(Layer newLayer) {
707                updateEnabledState();
708            }
709            @Override
710            public void layerRemoved(Layer oldLayer) {
711                updateEnabledState();
712            }
713    
714            @Override
715            public void executeMultikeyAction(int index, boolean repeat) {
716                Layer l = LayerListDialog.getLayerForIndex(index);
717                if (l != null) {
718                    execute(l);
719                }
720            }
721    
722            @Override
723            public List<MultikeyInfo> getMultikeyCombinations() {
724                return LayerListDialog.getLayerInfoByClass(Layer.class);
725            }
726    
727            @Override
728            public MultikeyInfo getLastMultikeyAction() {
729                return null; // Repeating action doesn't make much sense for activating
730            }
731        }
732    
733        /**
734         * The action to merge the currently selected layer into another layer.
735         */
736        public final class MergeAction extends AbstractAction implements IEnabledStateUpdating {
737            private  Layer layer;
738    
739            public MergeAction(Layer layer) throws IllegalArgumentException {
740                this();
741                CheckParameterUtil.ensureParameterNotNull(layer, "layer");
742                this.layer = layer;
743                putValue(NAME, tr("Merge"));
744                updateEnabledState();
745            }
746    
747            public MergeAction() {
748                putValue(NAME, tr("Merge"));
749                putValue(SMALL_ICON, ImageProvider.get("dialogs", "mergedown"));
750                putValue(SHORT_DESCRIPTION, tr("Merge this layer into another layer"));
751                putValue("help", HelpUtil.ht("/Dialog/LayerList#MergeLayer"));
752                updateEnabledState();
753            }
754    
755            @Override
756            public void actionPerformed(ActionEvent e) {
757                if (layer != null) {
758                    new MergeLayerAction().merge(layer);
759                } else {
760                    if (getModel().getSelectedLayers().size() == 1) {
761                        Layer selectedLayer = getModel().getSelectedLayers().get(0);
762                        new MergeLayerAction().merge(selectedLayer);
763                    } else {
764                        new MergeLayerAction().merge(getModel().getSelectedLayers());
765                    }
766                }
767            }
768    
769            protected boolean isActiveLayer(Layer layer) {
770                if (Main.map == null) return false;
771                if (Main.map.mapView == null) return false;
772                return Main.map.mapView.getActiveLayer() == layer;
773            }
774    
775            @Override
776            public void updateEnabledState() {
777                if (layer == null) {
778                    if (getModel().getSelectedLayers().isEmpty()) {
779                        setEnabled(false);
780                    } else  if (getModel().getSelectedLayers().size() > 1) {
781                        Layer firstLayer = getModel().getSelectedLayers().get(0);
782                        for (Layer l: getModel().getSelectedLayers()) {
783                            if (l != firstLayer && (!l.isMergable(firstLayer) || !firstLayer.isMergable(l))) {
784                                setEnabled(false);
785                                return;
786                            }
787                        }
788                        setEnabled(true);
789                    } else {
790                        Layer selectedLayer = getModel().getSelectedLayers().get(0);
791                        List<Layer> targets = getModel().getPossibleMergeTargets(selectedLayer);
792                        setEnabled(!targets.isEmpty());
793                    }
794                } else {
795                    List<Layer> targets = getModel().getPossibleMergeTargets(layer);
796                    setEnabled(!targets.isEmpty());
797                }
798            }
799        }
800    
801        /**
802         * The action to merge the currently selected layer into another layer.
803         */
804        public final class DuplicateAction extends AbstractAction implements IEnabledStateUpdating {
805            private  Layer layer;
806    
807            public DuplicateAction(Layer layer) throws IllegalArgumentException {
808                this();
809                CheckParameterUtil.ensureParameterNotNull(layer, "layer");
810                this.layer = layer;
811                updateEnabledState();
812            }
813    
814            public DuplicateAction() {
815                putValue(NAME, tr("Duplicate"));
816                putValue(SMALL_ICON, ImageProvider.get("dialogs", "duplicatelayer"));
817                putValue(SHORT_DESCRIPTION, tr("Duplicate this layer"));
818                putValue("help", HelpUtil.ht("/Dialog/LayerList#DuplicateLayer"));
819                updateEnabledState();
820            }
821    
822            private void duplicate(Layer layer) {
823                if (Main.map == null || Main.map.mapView == null)
824                    return;
825    
826                List<String> layerNames = new ArrayList<String>();
827                for (Layer l: Main.map.mapView.getAllLayers()) {
828                    layerNames.add(l.getName());
829                }
830                if (layer instanceof OsmDataLayer) {
831                    OsmDataLayer oldLayer = (OsmDataLayer)layer;
832                    // Translators: "Copy of {layer name}"
833                    String newName = tr("Copy of {0}", oldLayer.getName());
834                    int i = 2;
835                    while (layerNames.contains(newName)) {
836                        // Translators: "Copy {number} of {layer name}"
837                        newName = tr("Copy {1} of {0}", oldLayer.getName(), i);
838                        i++;
839                    }
840                    Main.main.addLayer(new OsmDataLayer(oldLayer.data.clone(), newName, null));
841                }
842            }
843    
844            @Override
845            public void actionPerformed(ActionEvent e) {
846                if (layer != null) {
847                    duplicate(layer);
848                } else {
849                    duplicate(getModel().getSelectedLayers().get(0));
850                }
851            }
852    
853            protected boolean isActiveLayer(Layer layer) {
854                if (Main.map == null || Main.map.mapView == null)
855                    return false;
856                return Main.map.mapView.getActiveLayer() == layer;
857            }
858    
859            @Override
860            public void updateEnabledState() {
861                if (layer == null) {
862                    if (getModel().getSelectedLayers().size() == 1) {
863                        setEnabled(getModel().getSelectedLayers().get(0) instanceof OsmDataLayer);
864                    } else {
865                        setEnabled(false);
866                    }
867                } else {
868                    setEnabled(layer instanceof OsmDataLayer);
869                }
870            }
871        }
872    
873        private static class ActiveLayerCheckBox extends JCheckBox {
874            public ActiveLayerCheckBox() {
875                setHorizontalAlignment(javax.swing.SwingConstants.CENTER);
876                ImageIcon blank = ImageProvider.get("dialogs/layerlist", "blank");
877                ImageIcon active = ImageProvider.get("dialogs/layerlist", "active");
878                setIcon(blank);
879                setSelectedIcon(active);
880                setRolloverIcon(blank);
881                setRolloverSelectedIcon(active);
882                setPressedIcon(ImageProvider.get("dialogs/layerlist", "active-pressed"));
883            }
884        }
885    
886        private static class LayerVisibleCheckBox extends JCheckBox {
887            private final ImageIcon icon_eye;
888            private final ImageIcon icon_eye_translucent;
889            private boolean isTranslucent;
890            public LayerVisibleCheckBox() {
891                setHorizontalAlignment(javax.swing.SwingConstants.RIGHT);
892                icon_eye = ImageProvider.get("dialogs/layerlist", "eye");
893                icon_eye_translucent = ImageProvider.get("dialogs/layerlist", "eye-translucent");
894                setIcon(ImageProvider.get("dialogs/layerlist", "eye-off"));
895                setPressedIcon(ImageProvider.get("dialogs/layerlist", "eye-pressed"));
896                setSelectedIcon(icon_eye);
897                isTranslucent = false;
898            }
899    
900            public void setTranslucent(boolean isTranslucent) {
901                if (this.isTranslucent == isTranslucent) return;
902                if (isTranslucent) {
903                    setSelectedIcon(icon_eye_translucent);
904                } else {
905                    setSelectedIcon(icon_eye);
906                }
907                this.isTranslucent = isTranslucent;
908            }
909    
910            public void updateStatus(Layer layer) {
911                boolean visible = layer.isVisible();
912                setSelected(visible);
913                setTranslucent(layer.getOpacity()<1.0);
914                setToolTipText(visible ? tr("layer is currently visible (click to hide layer)") : tr("layer is currently hidden (click to show layer)"));
915            }
916        }
917    
918        private static class ActiveLayerCellRenderer implements TableCellRenderer {
919            JCheckBox cb;
920            public ActiveLayerCellRenderer() {
921                cb = new ActiveLayerCheckBox();
922            }
923    
924            @Override
925            public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
926                boolean active =  value != null && (Boolean) value;
927                cb.setSelected(active);
928                cb.setToolTipText(active ? tr("this layer is the active layer") : tr("this layer is not currently active (click to activate)"));
929                return cb;
930            }
931        }
932    
933        private static class LayerVisibleCellRenderer implements TableCellRenderer {
934            LayerVisibleCheckBox cb;
935            public LayerVisibleCellRenderer() {
936                this.cb = new LayerVisibleCheckBox();
937            }
938    
939            @Override
940            public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
941                if (value != null) {
942                    cb.updateStatus((Layer)value);
943                }
944                return cb;
945            }
946        }
947    
948        private static class LayerVisibleCellEditor extends DefaultCellEditor {
949            LayerVisibleCheckBox cb;
950            public LayerVisibleCellEditor(LayerVisibleCheckBox cb) {
951                super(cb);
952                this.cb = cb;
953            }
954    
955            @Override
956            public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) {
957                cb.updateStatus((Layer)value);
958                return cb;
959            }
960        }
961    
962        private class LayerNameCellRenderer extends DefaultTableCellRenderer {
963    
964            protected boolean isActiveLayer(Layer layer) {
965                if (Main.map == null) return false;
966                if (Main.map.mapView == null) return false;
967                return Main.map.mapView.getActiveLayer() == layer;
968            }
969    
970            @Override
971            public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
972                if (value == null)
973                    return this;
974                Layer layer = (Layer)value;
975                JLabel label = (JLabel)super.getTableCellRendererComponent(table,
976                        layer.getName(), isSelected, hasFocus, row, column);
977                if (isActiveLayer(layer)) {
978                    label.setFont(label.getFont().deriveFont(Font.BOLD));
979                }
980                if(Main.pref.getBoolean("dialog.layer.colorname", true)) {
981                    Color c = layer.getColor(false);
982                    if(c != null) {
983                        Color oc = null;
984                        for(Layer l : model.getLayers()) {
985                            oc = l.getColor(false);
986                            if(oc != null) {
987                                if(oc.equals(c)) {
988                                    oc = null;
989                                } else {
990                                    break;
991                                }
992                            }
993                        }
994                        /* not more than one color, don't use coloring */
995                        if(oc == null) {
996                            c = null;
997                        }
998                    }
999                    if(c == null) {
1000                        c = Main.pref.getUIColor(isSelected ? "Table.selectionForeground" : "Table.foreground");
1001                    }
1002                    label.setForeground(c);
1003                }
1004                label.setIcon(layer.getIcon());
1005                label.setToolTipText(layer.getToolTipText());
1006                return label;
1007            }
1008        }
1009    
1010        private static class LayerNameCellEditor extends DefaultCellEditor {
1011            public LayerNameCellEditor(JTextField tf) {
1012                super(tf);
1013            }
1014    
1015            @Override
1016            public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) {
1017                JTextField tf = (JTextField) super.getTableCellEditorComponent(table, value, isSelected, row, column);
1018                tf.setText(value == null ? "" : ((Layer) value).getName());
1019                return tf;
1020            }
1021        }
1022    
1023        class PopupMenuHandler extends PopupMenuLauncher {
1024            @Override
1025            public void launch(MouseEvent evt) {
1026                Point p = evt.getPoint();
1027                int index = layerList.rowAtPoint(p);
1028                if (index < 0) return;
1029                if (!layerList.getCellRect(index, 2, false).contains(evt.getPoint()))
1030                    return;
1031                if (!layerList.isRowSelected(index)) {
1032                    layerList.setRowSelectionInterval(index, index);
1033                }
1034                Layer layer = model.getLayer(index);
1035                LayerListPopup menu = new LayerListPopup(getModel().getSelectedLayers(), layer);
1036                menu.show(layerList, p.x, p.y-3);
1037            }
1038        }
1039    
1040        /**
1041         * The action to move up the currently selected entries in the list.
1042         */
1043        class MoveUpAction extends AbstractAction implements  IEnabledStateUpdating{
1044            public MoveUpAction() {
1045                putValue(NAME, tr("Move up"));
1046                putValue(SMALL_ICON, ImageProvider.get("dialogs", "up"));
1047                putValue(SHORT_DESCRIPTION, tr("Move the selected layer one row up."));
1048                updateEnabledState();
1049            }
1050    
1051            @Override
1052            public void updateEnabledState() {
1053                setEnabled(model.canMoveUp());
1054            }
1055    
1056            @Override
1057            public void actionPerformed(ActionEvent e) {
1058                model.moveUp();
1059            }
1060        }
1061    
1062        /**
1063         * The action to move down the currently selected entries in the list.
1064         */
1065        class MoveDownAction extends AbstractAction implements IEnabledStateUpdating {
1066            public MoveDownAction() {
1067                putValue(NAME, tr("Move down"));
1068                putValue(SMALL_ICON, ImageProvider.get("dialogs", "down"));
1069                putValue(SHORT_DESCRIPTION, tr("Move the selected layer one row down."));
1070                updateEnabledState();
1071            }
1072    
1073            @Override
1074            public void updateEnabledState() {
1075                setEnabled(model.canMoveDown());
1076            }
1077    
1078            @Override
1079            public void actionPerformed(ActionEvent e) {
1080                model.moveDown();
1081            }
1082        }
1083    
1084        /**
1085         * Observer interface to be implemented by views using {@link LayerListModel}
1086         *
1087         */
1088        public interface LayerListModelListener {
1089            public void makeVisible(int index, Layer layer);
1090            public void refresh();
1091        }
1092    
1093        /**
1094         * The layer list model. The model manages a list of layers and provides methods for
1095         * moving layers up and down, for toggling their visibility, and for activating a layer.
1096         *
1097         * The model is a {@link TableModel} and it provides a {@link ListSelectionModel}. It expects
1098         * to be configured with a {@link DefaultListSelectionModel}. The selection model is used
1099         * to update the selection state of views depending on messages sent to the model.
1100         *
1101         * The model manages a list of {@link LayerListModelListener} which are mainly notified if
1102         * the model requires views to make a specific list entry visible.
1103         *
1104         * It also listens to {@link PropertyChangeEvent}s of every {@link Layer} it manages, in particular to
1105         * the properties {@link Layer#VISIBLE_PROP} and {@link Layer#NAME_PROP}.
1106         */
1107        public class LayerListModel extends AbstractTableModel implements MapView.LayerChangeListener, PropertyChangeListener {
1108            /** manages list selection state*/
1109            private DefaultListSelectionModel selectionModel;
1110            private CopyOnWriteArrayList<LayerListModelListener> listeners;
1111    
1112            /**
1113             * constructor
1114             *
1115             * @param selectionModel the list selection model
1116             */
1117            private LayerListModel(DefaultListSelectionModel selectionModel) {
1118                this.selectionModel = selectionModel;
1119                listeners = new CopyOnWriteArrayList<LayerListModelListener>();
1120            }
1121    
1122            /**
1123             * Adds a listener to this model
1124             *
1125             * @param listener the listener
1126             */
1127            public void addLayerListModelListener(LayerListModelListener listener) {
1128                if (listener != null) {
1129                    listeners.addIfAbsent(listener);
1130                }
1131            }
1132    
1133            /**
1134             * removes a listener from  this model
1135             * @param listener the listener
1136             *
1137             */
1138            public void removeLayerListModelListener(LayerListModelListener listener) {
1139                listeners.remove(listener);
1140            }
1141    
1142            /**
1143             * Fires a make visible event to listeners
1144             *
1145             * @param index the index of the row to make visible
1146             * @param layer the layer at this index
1147             * @see LayerListModelListener#makeVisible(int, Layer)
1148             */
1149            protected void fireMakeVisible(int index, Layer layer) {
1150                for (LayerListModelListener listener : listeners) {
1151                    listener.makeVisible(index, layer);
1152                }
1153            }
1154    
1155            /**
1156             * Fires a refresh event to listeners of this model
1157             *
1158             * @see LayerListModelListener#refresh()
1159             */
1160            protected void fireRefresh() {
1161                for (LayerListModelListener listener : listeners) {
1162                    listener.refresh();
1163                }
1164            }
1165    
1166            /**
1167             * Populates the model with the current layers managed by
1168             * {@link MapView}.
1169             *
1170             */
1171            public void populate() {
1172                for (Layer layer: getLayers()) {
1173                    // make sure the model is registered exactly once
1174                    //
1175                    layer.removePropertyChangeListener(this);
1176                    layer.addPropertyChangeListener(this);
1177                }
1178                fireTableDataChanged();
1179            }
1180    
1181            /**
1182             * Marks <code>layer</code> as selected layer. Ignored, if
1183             * layer is null.
1184             *
1185             * @param layer the layer.
1186             */
1187            public void setSelectedLayer(Layer layer) {
1188                if (layer == null)
1189                    return;
1190                int idx = getLayers().indexOf(layer);
1191                if (idx >= 0) {
1192                    selectionModel.setSelectionInterval(idx, idx);
1193                }
1194                ensureSelectedIsVisible();
1195            }
1196    
1197            /**
1198             * Replies the list of currently selected layers. Never null, but may
1199             * be empty.
1200             *
1201             * @return the list of currently selected layers. Never null, but may
1202             * be empty.
1203             */
1204            public List<Layer> getSelectedLayers() {
1205                ArrayList<Layer> selected = new ArrayList<Layer>();
1206                for (int i=0; i<getLayers().size(); i++) {
1207                    if (selectionModel.isSelectedIndex(i)) {
1208                        selected.add(getLayers().get(i));
1209                    }
1210                }
1211                return selected;
1212            }
1213    
1214            /**
1215             * Replies a the list of indices of the selected rows. Never null,
1216             * but may be empty.
1217             *
1218             * @return  the list of indices of the selected rows. Never null,
1219             * but may be empty.
1220             */
1221            public List<Integer> getSelectedRows() {
1222                ArrayList<Integer> selected = new ArrayList<Integer>();
1223                for (int i=0; i<getLayers().size();i++) {
1224                    if (selectionModel.isSelectedIndex(i)) {
1225                        selected.add(i);
1226                    }
1227                }
1228                return selected;
1229            }
1230    
1231            /**
1232             * Invoked if a layer managed by {@link MapView} is removed
1233             *
1234             * @param layer the layer which is removed
1235             */
1236            protected void onRemoveLayer(Layer layer) {
1237                if (layer == null)
1238                    return;
1239                layer.removePropertyChangeListener(this);
1240                final int size = getRowCount();
1241                final List<Integer> rows = getSelectedRows();
1242                GuiHelper.runInEDTAndWait(new Runnable() {
1243                    @Override
1244                    public void run() {
1245                        if (rows.isEmpty() && size > 0) {
1246                            selectionModel.setSelectionInterval(size-1, size-1);
1247                        }
1248                        fireTableDataChanged();
1249                        fireRefresh();
1250                        ensureActiveSelected();
1251                    }
1252                });
1253            }
1254    
1255            /**
1256             * Invoked when a layer managed by {@link MapView} is added
1257             *
1258             * @param layer the layer
1259             */
1260            protected void onAddLayer(Layer layer) {
1261                if (layer == null) return;
1262                layer.addPropertyChangeListener(this);
1263                fireTableDataChanged();
1264                int idx = getLayers().indexOf(layer);
1265                layerList.setRowHeight(idx, Math.max(16, layer.getIcon().getIconHeight()));
1266                selectionModel.setSelectionInterval(idx, idx);
1267                ensureSelectedIsVisible();
1268            }
1269    
1270            /**
1271             * Replies the first layer. Null if no layers are present
1272             *
1273             * @return the first layer. Null if no layers are present
1274             */
1275            public Layer getFirstLayer() {
1276                if (getRowCount() == 0) return null;
1277                return getLayers().get(0);
1278            }
1279    
1280            /**
1281             * Replies the layer at position <code>index</code>
1282             *
1283             * @param index the index
1284             * @return the layer at position <code>index</code>. Null,
1285             * if index is out of range.
1286             */
1287            public Layer getLayer(int index) {
1288                if (index < 0 || index >= getRowCount())
1289                    return null;
1290                return getLayers().get(index);
1291            }
1292    
1293            /**
1294             * Replies true if the currently selected layers can move up
1295             * by one position
1296             *
1297             * @return true if the currently selected layers can move up
1298             * by one position
1299             */
1300            public boolean canMoveUp() {
1301                List<Integer> sel = getSelectedRows();
1302                return !sel.isEmpty() && sel.get(0) > 0;
1303            }
1304    
1305            /**
1306             * Move up the currently selected layers by one position
1307             *
1308             */
1309            public void moveUp() {
1310                if (!canMoveUp()) return;
1311                List<Integer> sel = getSelectedRows();
1312                for (int row : sel) {
1313                    Layer l1 = getLayers().get(row);
1314                    Layer l2 = getLayers().get(row-1);
1315                    Main.map.mapView.moveLayer(l2,row);
1316                    Main.map.mapView.moveLayer(l1, row-1);
1317                }
1318                fireTableDataChanged();
1319                selectionModel.clearSelection();
1320                for (int row : sel) {
1321                    selectionModel.addSelectionInterval(row-1, row-1);
1322                }
1323                ensureSelectedIsVisible();
1324            }
1325    
1326            /**
1327             * Replies true if the currently selected layers can move down
1328             * by one position
1329             *
1330             * @return true if the currently selected layers can move down
1331             * by one position
1332             */
1333            public boolean canMoveDown() {
1334                List<Integer> sel = getSelectedRows();
1335                return !sel.isEmpty() && sel.get(sel.size()-1) < getLayers().size()-1;
1336            }
1337    
1338            /**
1339             * Move down the currently selected layers by one position
1340             *
1341             */
1342            public void moveDown() {
1343                if (!canMoveDown()) return;
1344                List<Integer> sel = getSelectedRows();
1345                Collections.reverse(sel);
1346                for (int row : sel) {
1347                    Layer l1 = getLayers().get(row);
1348                    Layer l2 = getLayers().get(row+1);
1349                    Main.map.mapView.moveLayer(l1, row+1);
1350                    Main.map.mapView.moveLayer(l2, row);
1351                }
1352                fireTableDataChanged();
1353                selectionModel.clearSelection();
1354                for (int row : sel) {
1355                    selectionModel.addSelectionInterval(row+1, row+1);
1356                }
1357                ensureSelectedIsVisible();
1358            }
1359    
1360            /**
1361             * Make sure the first of the selected layers is visible in the
1362             * views of this model.
1363             *
1364             */
1365            protected void ensureSelectedIsVisible() {
1366                int index = selectionModel.getMinSelectionIndex();
1367                if (index < 0) return;
1368                if (index >= getLayers().size()) return;
1369                Layer layer = getLayers().get(index);
1370                fireMakeVisible(index, layer);
1371            }
1372    
1373            /**
1374             * Replies a list of layers which are possible merge targets
1375             * for <code>source</code>
1376             *
1377             * @param source the source layer
1378             * @return a list of layers which are possible merge targets
1379             * for <code>source</code>. Never null, but can be empty.
1380             */
1381            public List<Layer> getPossibleMergeTargets(Layer source) {
1382                ArrayList<Layer> targets = new ArrayList<Layer>();
1383                if (source == null)
1384                    return targets;
1385                for (Layer target : getLayers()) {
1386                    if (source == target) {
1387                        continue;
1388                    }
1389                    if (target.isMergable(source) && source.isMergable(target)) {
1390                        targets.add(target);
1391                    }
1392                }
1393                return targets;
1394            }
1395    
1396            /**
1397             * Replies the list of layers currently managed by {@link MapView}.
1398             * Never null, but can be empty.
1399             *
1400             * @return the list of layers currently managed by {@link MapView}.
1401             * Never null, but can be empty.
1402             */
1403            public List<Layer> getLayers() {
1404                if (Main.map == null || Main.map.mapView == null)
1405                    return Collections.<Layer>emptyList();
1406                return Main.map.mapView.getAllLayersAsList();
1407            }
1408    
1409            /**
1410             * Ensures that at least one layer is selected in the layer dialog
1411             *
1412             */
1413            protected void ensureActiveSelected() {
1414                if (getLayers().isEmpty())
1415                    return;
1416                final Layer activeLayer = getActiveLayer();
1417                if (activeLayer != null) {
1418                    // there's an active layer - select it and make it
1419                    // visible
1420                    int idx = getLayers().indexOf(activeLayer);
1421                    selectionModel.setSelectionInterval(idx, idx);
1422                    ensureSelectedIsVisible();
1423                } else {
1424                    // no active layer - select the first one and make
1425                    // it visible
1426                    selectionModel.setSelectionInterval(0, 0);
1427                    ensureSelectedIsVisible();
1428                }
1429            }
1430    
1431            /**
1432             * Replies the active layer. null, if no active layer is available
1433             *
1434             * @return the active layer. null, if no active layer is available
1435             */
1436            protected Layer getActiveLayer() {
1437                if (Main.map == null || Main.map.mapView == null) return null;
1438                return Main.map.mapView.getActiveLayer();
1439            }
1440    
1441            /* ------------------------------------------------------------------------------ */
1442            /* Interface TableModel                                                           */
1443            /* ------------------------------------------------------------------------------ */
1444    
1445            @Override
1446            public int getRowCount() {
1447                List<Layer> layers = getLayers();
1448                if (layers == null) return 0;
1449                return layers.size();
1450            }
1451    
1452            @Override
1453            public int getColumnCount() {
1454                return 3;
1455            }
1456    
1457            @Override
1458            public Object getValueAt(int row, int col) {
1459                if (row >= 0 && row < getLayers().size()) {
1460                    switch (col) {
1461                    case 0: return getLayers().get(row) == getActiveLayer();
1462                    case 1: return getLayers().get(row);
1463                    case 2: return getLayers().get(row);
1464                    default: throw new RuntimeException();
1465                    }
1466                }
1467                return null;
1468            }
1469    
1470            @Override
1471            public boolean isCellEditable(int row, int col) {
1472                if (col == 0 && getActiveLayer() == getLayers().get(row))
1473                    return false;
1474                return true;
1475            }
1476    
1477            @Override
1478            public void setValueAt(Object value, int row, int col) {
1479                Layer l = getLayers().get(row);
1480                switch (col) {
1481                case 0:
1482                    Main.map.mapView.setActiveLayer(l);
1483                    l.setVisible(true);
1484                    break;
1485                case 1:
1486                    l.setVisible((Boolean) value);
1487                    break;
1488                case 2:
1489                    l.setName((String) value);
1490                    break;
1491                default: throw new RuntimeException();
1492                }
1493                fireTableCellUpdated(row, col);
1494            }
1495    
1496            /* ------------------------------------------------------------------------------ */
1497            /* Interface LayerChangeListener                                                  */
1498            /* ------------------------------------------------------------------------------ */
1499            @Override
1500            public void activeLayerChange(final Layer oldLayer, final Layer newLayer) {
1501                GuiHelper.runInEDTAndWait(new Runnable() {
1502                    @Override
1503                    public void run() {
1504                        if (oldLayer != null) {
1505                            int idx = getLayers().indexOf(oldLayer);
1506                            if (idx >= 0) {
1507                                fireTableRowsUpdated(idx,idx);
1508                            }
1509                        }
1510    
1511                        if (newLayer != null) {
1512                            int idx = getLayers().indexOf(newLayer);
1513                            if (idx >= 0) {
1514                                fireTableRowsUpdated(idx,idx);
1515                            }
1516                        }
1517                        ensureActiveSelected();
1518                    }
1519                });
1520            }
1521    
1522            @Override
1523            public void layerAdded(Layer newLayer) {
1524                onAddLayer(newLayer);
1525            }
1526    
1527            @Override
1528            public void layerRemoved(final Layer oldLayer) {
1529                onRemoveLayer(oldLayer);
1530            }
1531    
1532            /* ------------------------------------------------------------------------------ */
1533            /* Interface PropertyChangeListener                                               */
1534            /* ------------------------------------------------------------------------------ */
1535            @Override
1536            public void propertyChange(PropertyChangeEvent evt) {
1537                if (evt.getSource() instanceof Layer) {
1538                    Layer layer = (Layer)evt.getSource();
1539                    final int idx = getLayers().indexOf(layer);
1540                    if (idx < 0) return;
1541                    fireRefresh();
1542                }
1543            }
1544        }
1545    
1546        static class LayerList extends JTable {
1547            public LayerList(TableModel dataModel) {
1548                super(dataModel);
1549            }
1550    
1551            public void scrollToVisible(int row, int col) {
1552                if (!(getParent() instanceof JViewport))
1553                    return;
1554                JViewport viewport = (JViewport) getParent();
1555                Rectangle rect = getCellRect(row, col, true);
1556                Point pt = viewport.getViewPosition();
1557                rect.setLocation(rect.x - pt.x, rect.y - pt.y);
1558                viewport.scrollRectToVisible(rect);
1559            }
1560        }
1561    
1562        /**
1563         * Creates a {@link ShowHideLayerAction} for <code>layer</code> in the
1564         * context of this {@link LayerListDialog}.
1565         *
1566         * @param layer the layer
1567         * @return the action
1568         */
1569        public ShowHideLayerAction createShowHideLayerAction() {
1570            ShowHideLayerAction act = new ShowHideLayerAction(true);
1571            act.putValue(Action.NAME, tr("Show/Hide"));
1572            return act;
1573        }
1574    
1575        /**
1576         * Creates a {@link DeleteLayerAction} for <code>layer</code> in the
1577         * context of this {@link LayerListDialog}.
1578         *
1579         * @param layer the layer
1580         * @return the action
1581         */
1582        public DeleteLayerAction createDeleteLayerAction() {
1583            // the delete layer action doesn't depend on the current layer
1584            return new DeleteLayerAction();
1585        }
1586    
1587        /**
1588         * Creates a {@link ActivateLayerAction} for <code>layer</code> in the
1589         * context of this {@link LayerListDialog}.
1590         *
1591         * @param layer the layer
1592         * @return the action
1593         */
1594        public ActivateLayerAction createActivateLayerAction(Layer layer) {
1595            return new ActivateLayerAction(layer);
1596        }
1597    
1598        /**
1599         * Creates a {@link MergeLayerAction} for <code>layer</code> in the
1600         * context of this {@link LayerListDialog}.
1601         *
1602         * @param layer the layer
1603         * @return the action
1604         */
1605        public MergeAction createMergeLayerAction(Layer layer) {
1606            return new MergeAction(layer);
1607        }
1608    
1609        public static Layer getLayerForIndex(int index) {
1610    
1611            if (!Main.isDisplayingMapView())
1612                return null;
1613    
1614            List<Layer> layers = Main.map.mapView.getAllLayersAsList();
1615    
1616            if (index < layers.size() && index >= 0)
1617                return layers.get(index);
1618            else
1619                return null;
1620        }
1621    
1622        // This is not Class<? extends Layer> on purpose, to allow asking for layers implementing some interface
1623        public static List<MultikeyInfo> getLayerInfoByClass(Class<?> layerClass) {
1624    
1625            List<MultikeyInfo> result = new ArrayList<MultikeyShortcutAction.MultikeyInfo>();
1626    
1627            if (!Main.isDisplayingMapView())
1628                return result;
1629    
1630            List<Layer> layers = Main.map.mapView.getAllLayersAsList();
1631    
1632            int index = 0;
1633            for (Layer l: layers) {
1634                if (layerClass.isAssignableFrom(l.getClass())) {
1635                    result.add(new MultikeyInfo(index, l.getName()));
1636                }
1637                index++;
1638            }
1639    
1640            return result;
1641        }
1642    
1643        public static boolean isLayerValid(Layer l) {
1644            if (l == null)
1645                return false;
1646    
1647            if (!Main.isDisplayingMapView())
1648                return false;
1649    
1650            return Main.map.mapView.getAllLayersAsList().indexOf(l) >= 0;
1651        }
1652    
1653        public static MultikeyInfo getLayerInfo(Layer l) {
1654    
1655            if (l == null)
1656                return null;
1657    
1658            if (!Main.isDisplayingMapView())
1659                return null;
1660    
1661            int index = Main.map.mapView.getAllLayersAsList().indexOf(l);
1662            if (index < 0)
1663                return null;
1664    
1665            return new MultikeyInfo(index, l.getName());
1666        }
1667    }