// License: GPL. For details, see LICENSE file. package org.openstreetmap.josm.gui.dialogs; import static org.openstreetmap.josm.tools.I18n.tr; import java.awt.Color; import java.awt.Component; import java.awt.Dimension; import java.awt.Font; import java.awt.Point; import java.awt.Rectangle; import java.awt.event.ActionEvent; import java.awt.event.InputEvent; import java.awt.event.KeyEvent; import java.awt.event.MouseEvent; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; import javax.swing.AbstractAction; import javax.swing.Action; import javax.swing.DefaultCellEditor; import javax.swing.DefaultListSelectionModel; import javax.swing.ImageIcon; import javax.swing.JCheckBox; import javax.swing.JComponent; import javax.swing.JLabel; import javax.swing.JMenuItem; import javax.swing.JPopupMenu; import javax.swing.JSlider; import javax.swing.JTable; import javax.swing.JViewport; import javax.swing.KeyStroke; import javax.swing.ListSelectionModel; import javax.swing.UIManager; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import javax.swing.event.ListDataEvent; import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; import javax.swing.event.TableModelEvent; import javax.swing.event.TableModelListener; import javax.swing.table.AbstractTableModel; import javax.swing.table.DefaultTableCellRenderer; import javax.swing.table.TableCellRenderer; import javax.swing.table.TableModel; import org.openstreetmap.josm.Main; import org.openstreetmap.josm.actions.MergeLayerAction; import org.openstreetmap.josm.gui.MapFrame; import org.openstreetmap.josm.gui.MapView; import org.openstreetmap.josm.gui.SideButton; import org.openstreetmap.josm.gui.help.HelpUtil; import org.openstreetmap.josm.gui.layer.JumpToMarkerActions; import org.openstreetmap.josm.gui.layer.Layer; import org.openstreetmap.josm.gui.layer.Layer.LayerAction; import org.openstreetmap.josm.gui.layer.OsmDataLayer; import org.openstreetmap.josm.gui.util.GuiHelper; import org.openstreetmap.josm.gui.widgets.JosmTextField; import org.openstreetmap.josm.gui.widgets.PopupMenuLauncher; import org.openstreetmap.josm.tools.CheckParameterUtil; import org.openstreetmap.josm.tools.ImageProvider; import org.openstreetmap.josm.tools.InputMapUtils; import org.openstreetmap.josm.tools.MultikeyActionsHandler; import org.openstreetmap.josm.tools.MultikeyShortcutAction; import org.openstreetmap.josm.tools.MultikeyShortcutAction.MultikeyInfo; import org.openstreetmap.josm.tools.Shortcut; /** * This is a toggle dialog which displays the list of layers. Actions allow to * change the ordering of the layers, to hide/show layers, to activate layers, * and to delete layers. * */ public class LayerListDialog extends ToggleDialog { /** the unique instance of the dialog */ static private LayerListDialog instance; /** * Creates the instance of the dialog. It's connected to the map frame mapFrame * * @param mapFrame the map frame */ static public void createInstance(MapFrame mapFrame) { if (instance != null) throw new IllegalStateException("Dialog was already created"); instance = new LayerListDialog(mapFrame); } /** * Replies the instance of the dialog * * @return the instance of the dialog * @throws IllegalStateException thrown, if the dialog is not created yet * @see #createInstance(MapFrame) */ static public LayerListDialog getInstance() throws IllegalStateException { if (instance == null) throw new IllegalStateException("Dialog not created yet. Invoke createInstance() first"); return instance; } /** the model for the layer list */ private LayerListModel model; /** the selection model */ private DefaultListSelectionModel selectionModel; /** the list of layers (technically its a JTable, but appears like a list) */ private LayerList layerList; private SideButton opacityButton; ActivateLayerAction activateLayerAction; ShowHideLayerAction showHideLayerAction; //TODO This duplicates ShowHide actions functionality /** stores which layer index to toggle and executes the ShowHide action if the layer is present */ private final class ToggleLayerIndexVisibility extends AbstractAction { int layerIndex = -1; public ToggleLayerIndexVisibility(int layerIndex) { this.layerIndex = layerIndex; } @Override public void actionPerformed(ActionEvent e) { final Layer l = model.getLayer(model.getRowCount() - layerIndex - 1); if(l != null) { l.toggleVisible(); } } } private final Shortcut[] visibilityToggleShortcuts = new Shortcut[10]; private final ToggleLayerIndexVisibility[] visibilityToggleActions = new ToggleLayerIndexVisibility[10]; /** * registers (shortcut to toggle right hand side toggle dialogs)+(number keys) shortcuts * to toggle the visibility of the first ten layers. */ private final void createVisibilityToggleShortcuts() { final int[] k = { KeyEvent.VK_1, KeyEvent.VK_2, KeyEvent.VK_3, KeyEvent.VK_4, KeyEvent.VK_5, KeyEvent.VK_6, KeyEvent.VK_7, KeyEvent.VK_8, KeyEvent.VK_9, KeyEvent.VK_0 }; for(int i=0; i < 10; i++) { visibilityToggleShortcuts[i] = Shortcut.registerShortcut("subwindow:layers:toggleLayer" + (i+1), tr("Toggle visibility of layer: {0}", (i+1)), k[i], Shortcut.ALT); visibilityToggleActions[i] = new ToggleLayerIndexVisibility(i); Main.registerActionShortcut(visibilityToggleActions[i], visibilityToggleShortcuts[i]); } } /** * Create an layer list and attach it to the given mapView. */ protected LayerListDialog(MapFrame mapFrame) { super(tr("Layers"), "layerlist", tr("Open a list of all loaded layers."), Shortcut.registerShortcut("subwindow:layers", tr("Toggle: {0}", tr("Layers")), KeyEvent.VK_L, Shortcut.ALT_SHIFT), 100, true); // create the models // selectionModel = new DefaultListSelectionModel(); selectionModel.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); model = new LayerListModel(selectionModel); // create the list control // layerList = new LayerList(model); layerList.setSelectionModel(selectionModel); layerList.addMouseListener(new PopupMenuHandler()); layerList.setBackground(UIManager.getColor("Button.background")); layerList.putClientProperty("terminateEditOnFocusLost", true); layerList.putClientProperty("JTable.autoStartsEdit", false); layerList.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); layerList.setTableHeader(null); layerList.setShowGrid(false); layerList.setIntercellSpacing(new Dimension(0, 0)); layerList.getColumnModel().getColumn(0).setCellRenderer(new ActiveLayerCellRenderer()); layerList.getColumnModel().getColumn(0).setCellEditor(new DefaultCellEditor(new ActiveLayerCheckBox())); layerList.getColumnModel().getColumn(0).setMaxWidth(12); layerList.getColumnModel().getColumn(0).setPreferredWidth(12); layerList.getColumnModel().getColumn(0).setResizable(false); layerList.getColumnModel().getColumn(1).setCellRenderer(new LayerVisibleCellRenderer()); layerList.getColumnModel().getColumn(1).setCellEditor(new LayerVisibleCellEditor(new LayerVisibleCheckBox())); layerList.getColumnModel().getColumn(1).setMaxWidth(16); layerList.getColumnModel().getColumn(1).setPreferredWidth(16); layerList.getColumnModel().getColumn(1).setResizable(false); layerList.getColumnModel().getColumn(2).setCellRenderer(new LayerNameCellRenderer()); layerList.getColumnModel().getColumn(2).setCellEditor(new LayerNameCellEditor(new JosmTextField())); for (KeyStroke ks : new KeyStroke[] { KeyStroke.getKeyStroke(KeyEvent.VK_C, InputEvent.CTRL_MASK), KeyStroke.getKeyStroke(KeyEvent.VK_V, InputEvent.CTRL_MASK), KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, InputEvent.SHIFT_MASK), KeyStroke.getKeyStroke(KeyEvent.VK_UP, InputEvent.SHIFT_MASK), KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, InputEvent.SHIFT_MASK), KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, InputEvent.SHIFT_MASK), KeyStroke.getKeyStroke(KeyEvent.VK_PAGE_UP, 0), KeyStroke.getKeyStroke(KeyEvent.VK_PAGE_DOWN, 0), }) { layerList.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(ks, new Object()); } // init the model // final MapView mapView = mapFrame.mapView; model.populate(); model.setSelectedLayer(mapView.getActiveLayer()); model.addLayerListModelListener( new LayerListModelListener() { @Override public void makeVisible(int row, Layer layer) { layerList.scrollToVisible(row, 0); layerList.repaint(); } @Override public void refresh() { layerList.repaint(); } } ); // -- move up action MoveUpAction moveUpAction = new MoveUpAction(); adaptTo(moveUpAction, model); adaptTo(moveUpAction,selectionModel); // -- move down action MoveDownAction moveDownAction = new MoveDownAction(); adaptTo(moveDownAction, model); adaptTo(moveDownAction,selectionModel); // -- activate action activateLayerAction = new ActivateLayerAction(); activateLayerAction.updateEnabledState(); MultikeyActionsHandler.getInstance().addAction(activateLayerAction); adaptTo(activateLayerAction, selectionModel); JumpToMarkerActions.initialize(); // -- show hide action showHideLayerAction = new ShowHideLayerAction(); MultikeyActionsHandler.getInstance().addAction(showHideLayerAction); adaptTo(showHideLayerAction, selectionModel); //-- layer opacity action LayerOpacityAction layerOpacityAction = new LayerOpacityAction(); adaptTo(layerOpacityAction, selectionModel); opacityButton = new SideButton(layerOpacityAction, false); // -- merge layer action MergeAction mergeLayerAction = new MergeAction(); adaptTo(mergeLayerAction, model); adaptTo(mergeLayerAction,selectionModel); // -- duplicate layer action DuplicateAction duplicateLayerAction = new DuplicateAction(); adaptTo(duplicateLayerAction, model); adaptTo(duplicateLayerAction, selectionModel); //-- delete layer action DeleteLayerAction deleteLayerAction = new DeleteLayerAction(); layerList.getActionMap().put("deleteLayer", deleteLayerAction); adaptTo(deleteLayerAction, selectionModel); getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put( KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0),"delete" ); getActionMap().put("delete", deleteLayerAction); // Activate layer on Enter key press InputMapUtils.addEnterAction(layerList, new AbstractAction() { @Override public void actionPerformed(ActionEvent e) { activateLayerAction.actionPerformed(null); layerList.requestFocus(); } }); // Show/Activate layer on Enter key press InputMapUtils.addSpacebarAction(layerList, showHideLayerAction); createLayout(layerList, true, Arrays.asList(new SideButton[] { new SideButton(moveUpAction, false), new SideButton(moveDownAction, false), new SideButton(activateLayerAction, false), new SideButton(showHideLayerAction, false), opacityButton, new SideButton(mergeLayerAction, false), new SideButton(duplicateLayerAction, false), new SideButton(deleteLayerAction, false) })); createVisibilityToggleShortcuts(); } @Override public void showNotify() { MapView.addLayerChangeListener(activateLayerAction); MapView.addLayerChangeListener(model); model.populate(); } @Override public void hideNotify() { MapView.removeLayerChangeListener(model); MapView.removeLayerChangeListener(activateLayerAction); } public LayerListModel getModel() { return model; } protected interface IEnabledStateUpdating { void updateEnabledState(); } /** * Wires listener to listSelectionModel in such a way, that * listener receives a {@link IEnabledStateUpdating#updateEnabledState()} * on every {@link ListSelectionEvent}. * * @param listener the listener * @param listSelectionModel the source emitting {@link ListSelectionEvent}s */ protected void adaptTo(final IEnabledStateUpdating listener, ListSelectionModel listSelectionModel) { listSelectionModel.addListSelectionListener( new ListSelectionListener() { @Override public void valueChanged(ListSelectionEvent e) { listener.updateEnabledState(); } } ); } /** * Wires listener to listModel in such a way, that * listener receives a {@link IEnabledStateUpdating#updateEnabledState()} * on every {@link ListDataEvent}. * * @param listener the listener * @param listModel the source emitting {@link ListDataEvent}s */ protected void adaptTo(final IEnabledStateUpdating listener, LayerListModel listModel) { listModel.addTableModelListener( new TableModelListener() { @Override public void tableChanged(TableModelEvent e) { listener.updateEnabledState(); } } ); } @Override public void destroy() { for(int i=0; i < 10; i++) { Main.unregisterActionShortcut(visibilityToggleActions[i], visibilityToggleShortcuts[i]); } MultikeyActionsHandler.getInstance().removeAction(activateLayerAction); MultikeyActionsHandler.getInstance().removeAction(showHideLayerAction); JumpToMarkerActions.unregisterActions(); super.destroy(); instance = null; } /** * The action to delete the currently selected layer */ public final class DeleteLayerAction extends AbstractAction implements IEnabledStateUpdating, LayerAction { /** * Creates a {@link DeleteLayerAction} which will delete the currently * selected layers in the layer dialog. * */ public DeleteLayerAction() { putValue(SMALL_ICON,ImageProvider.get("dialogs", "delete")); putValue(SHORT_DESCRIPTION, tr("Delete the selected layers.")); putValue(NAME, tr("Delete")); putValue("help", HelpUtil.ht("/Dialog/LayerList#DeleteLayer")); updateEnabledState(); } @Override public void actionPerformed(ActionEvent e) { List selectedLayers = getModel().getSelectedLayers(); if (selectedLayers.isEmpty()) return; if (!Main.saveUnsavedModifications(selectedLayers, false)) return; for (Layer l: selectedLayers) { Main.main.removeLayer(l); } } @Override public void updateEnabledState() { setEnabled(! getModel().getSelectedLayers().isEmpty()); } @Override public Component createMenuComponent() { return new JMenuItem(this); } @Override public boolean supportLayers(List layers) { return true; } @Override public boolean equals(Object obj) { return obj instanceof DeleteLayerAction; } @Override public int hashCode() { return getClass().hashCode(); } } public final class ShowHideLayerAction extends AbstractAction implements IEnabledStateUpdating, LayerAction, MultikeyShortcutAction { private WeakReference lastLayer; private Shortcut multikeyShortcut; /** * Creates a {@link ShowHideLayerAction} which will toggle the visibility of * the currently selected layers * */ public ShowHideLayerAction(boolean init) { putValue(NAME, tr("Show/hide")); putValue(SMALL_ICON, ImageProvider.get("dialogs", "showhide")); putValue(SHORT_DESCRIPTION, tr("Toggle visible state of the selected layer.")); putValue("help", HelpUtil.ht("/Dialog/LayerList#ShowHideLayer")); multikeyShortcut = Shortcut.registerShortcut("core_multikey:showHideLayer", tr("Multikey: {0}", tr("Show/hide layer")), KeyEvent.VK_S, Shortcut.SHIFT); multikeyShortcut.setAccelerator(this); if (init) { updateEnabledState(); } } public ShowHideLayerAction() { this(true); } @Override public Shortcut getMultikeyShortcut() { return multikeyShortcut; } @Override public void actionPerformed(ActionEvent e) { for(Layer l : model.getSelectedLayers()) { l.toggleVisible(); } } @Override public void executeMultikeyAction(int index, boolean repeat) { Layer l = LayerListDialog.getLayerForIndex(index); if (l != null) { l.toggleVisible(); lastLayer = new WeakReference(l); } else if (repeat && lastLayer != null) { l = lastLayer.get(); if (LayerListDialog.isLayerValid(l)) { l.toggleVisible(); } } } @Override public void updateEnabledState() { setEnabled(!model.getSelectedLayers().isEmpty()); } @Override public Component createMenuComponent() { return new JMenuItem(this); } @Override public boolean supportLayers(List layers) { return true; } @Override public boolean equals(Object obj) { return obj instanceof ShowHideLayerAction; } @Override public int hashCode() { return getClass().hashCode(); } @Override public List getMultikeyCombinations() { return LayerListDialog.getLayerInfoByClass(Layer.class); } @Override public MultikeyInfo getLastMultikeyAction() { if (lastLayer != null) return LayerListDialog.getLayerInfo(lastLayer.get()); return null; } } public final class LayerOpacityAction extends AbstractAction implements IEnabledStateUpdating, LayerAction { private Layer layer; private JPopupMenu popup; private JSlider slider = new JSlider(JSlider.VERTICAL); /** * Creates a {@link LayerOpacityAction} which allows to chenge the * opacity of one or more layers. * * @param layer the layer. Must not be null. * @exception IllegalArgumentException thrown, if layer is null */ public LayerOpacityAction(Layer layer) throws IllegalArgumentException { this(); putValue(NAME, tr("Opacity")); CheckParameterUtil.ensureParameterNotNull(layer, "layer"); this.layer = layer; updateEnabledState(); } /** * Creates a {@link ShowHideLayerAction} which will toggle the visibility of * the currently selected layers * */ public LayerOpacityAction() { putValue(NAME, tr("Opacity")); putValue(SHORT_DESCRIPTION, tr("Adjust opacity of the layer.")); putValue(SMALL_ICON, ImageProvider.get("dialogs/layerlist", "transparency")); updateEnabledState(); popup = new JPopupMenu(); slider.addChangeListener(new ChangeListener() { @Override public void stateChanged(ChangeEvent e) { setOpacity((double)slider.getValue()/100); } }); popup.add(slider); } private void setOpacity(double value) { if (!isEnabled()) return; if (layer != null) { layer.setOpacity(value); } else { for(Layer layer: model.getSelectedLayers()) { layer.setOpacity(value); } } } private double getOpacity() { if (layer != null) return layer.getOpacity(); else { double opacity = 0; List layers = model.getSelectedLayers(); for(Layer layer: layers) { opacity += layer.getOpacity(); } return opacity / layers.size(); } } @Override public void actionPerformed(ActionEvent e) { slider.setValue((int)Math.round(getOpacity()*100)); if (e.getSource() == opacityButton) { popup.show(opacityButton, 0, opacityButton.getHeight()); } else { // Action can be trigger either by opacity button or by popup menu (in case toggle buttons are hidden). // In that case, show it in the middle of screen (because opacityButton is not visible) popup.show(Main.parent, Main.parent.getWidth() / 2, (Main.parent.getHeight() - popup.getHeight()) / 2); } } @Override public void updateEnabledState() { if (layer == null) { setEnabled(! getModel().getSelectedLayers().isEmpty()); } else { setEnabled(true); } } @Override public Component createMenuComponent() { return new JMenuItem(this); } @Override public boolean supportLayers(List layers) { return true; } @Override public boolean equals(Object obj) { return obj instanceof LayerOpacityAction; } @Override public int hashCode() { return getClass().hashCode(); } } /** * The action to activate the currently selected layer */ public final class ActivateLayerAction extends AbstractAction implements IEnabledStateUpdating, MapView.LayerChangeListener, MultikeyShortcutAction{ private Layer layer; private Shortcut multikeyShortcut; public ActivateLayerAction(Layer layer) { this(); CheckParameterUtil.ensureParameterNotNull(layer, "layer"); this.layer = layer; putValue(NAME, tr("Activate")); updateEnabledState(); } public ActivateLayerAction() { putValue(NAME, tr("Activate")); putValue(SMALL_ICON, ImageProvider.get("dialogs", "activate")); putValue(SHORT_DESCRIPTION, tr("Activate the selected layer")); multikeyShortcut = Shortcut.registerShortcut("core_multikey:activateLayer", tr("Multikey: {0}", tr("Activate layer")), KeyEvent.VK_A, Shortcut.SHIFT); multikeyShortcut.setAccelerator(this); putValue("help", HelpUtil.ht("/Dialog/LayerList#ActivateLayer")); } @Override public Shortcut getMultikeyShortcut() { return multikeyShortcut; } @Override public void actionPerformed(ActionEvent e) { Layer toActivate; if (layer != null) { toActivate = layer; } else { toActivate = model.getSelectedLayers().get(0); } execute(toActivate); } private void execute(Layer layer) { // model is going to be updated via LayerChangeListener // and PropertyChangeEvents Main.map.mapView.setActiveLayer(layer); layer.setVisible(true); } protected boolean isActiveLayer(Layer layer) { if (Main.map == null) return false; if (Main.map.mapView == null) return false; return Main.map.mapView.getActiveLayer() == layer; } @Override public void updateEnabledState() { GuiHelper.runInEDTAndWait(new Runnable() { @Override public void run() { if (layer == null) { if (getModel().getSelectedLayers().size() != 1) { setEnabled(false); return; } Layer selectedLayer = getModel().getSelectedLayers().get(0); setEnabled(!isActiveLayer(selectedLayer)); } else { setEnabled(!isActiveLayer(layer)); } } }); } @Override public void activeLayerChange(Layer oldLayer, Layer newLayer) { updateEnabledState(); } @Override public void layerAdded(Layer newLayer) { updateEnabledState(); } @Override public void layerRemoved(Layer oldLayer) { updateEnabledState(); } @Override public void executeMultikeyAction(int index, boolean repeat) { Layer l = LayerListDialog.getLayerForIndex(index); if (l != null) { execute(l); } } @Override public List getMultikeyCombinations() { return LayerListDialog.getLayerInfoByClass(Layer.class); } @Override public MultikeyInfo getLastMultikeyAction() { return null; // Repeating action doesn't make much sense for activating } } /** * The action to merge the currently selected layer into another layer. */ public final class MergeAction extends AbstractAction implements IEnabledStateUpdating { private Layer layer; public MergeAction(Layer layer) throws IllegalArgumentException { this(); CheckParameterUtil.ensureParameterNotNull(layer, "layer"); this.layer = layer; putValue(NAME, tr("Merge")); updateEnabledState(); } public MergeAction() { putValue(NAME, tr("Merge")); putValue(SMALL_ICON, ImageProvider.get("dialogs", "mergedown")); putValue(SHORT_DESCRIPTION, tr("Merge this layer into another layer")); putValue("help", HelpUtil.ht("/Dialog/LayerList#MergeLayer")); updateEnabledState(); } @Override public void actionPerformed(ActionEvent e) { if (layer != null) { new MergeLayerAction().merge(layer); } else { if (getModel().getSelectedLayers().size() == 1) { Layer selectedLayer = getModel().getSelectedLayers().get(0); new MergeLayerAction().merge(selectedLayer); } else { new MergeLayerAction().merge(getModel().getSelectedLayers()); } } } protected boolean isActiveLayer(Layer layer) { if (Main.map == null) return false; if (Main.map.mapView == null) return false; return Main.map.mapView.getActiveLayer() == layer; } @Override public void updateEnabledState() { if (layer == null) { if (getModel().getSelectedLayers().isEmpty()) { setEnabled(false); } else if (getModel().getSelectedLayers().size() > 1) { Layer firstLayer = getModel().getSelectedLayers().get(0); for (Layer l: getModel().getSelectedLayers()) { if (l != firstLayer && (!l.isMergable(firstLayer) || !firstLayer.isMergable(l))) { setEnabled(false); return; } } setEnabled(true); } else { Layer selectedLayer = getModel().getSelectedLayers().get(0); List targets = getModel().getPossibleMergeTargets(selectedLayer); setEnabled(!targets.isEmpty()); } } else { List targets = getModel().getPossibleMergeTargets(layer); setEnabled(!targets.isEmpty()); } } } /** * The action to merge the currently selected layer into another layer. */ public final class DuplicateAction extends AbstractAction implements IEnabledStateUpdating { private Layer layer; public DuplicateAction(Layer layer) throws IllegalArgumentException { this(); CheckParameterUtil.ensureParameterNotNull(layer, "layer"); this.layer = layer; updateEnabledState(); } public DuplicateAction() { putValue(NAME, tr("Duplicate")); putValue(SMALL_ICON, ImageProvider.get("dialogs", "duplicatelayer")); putValue(SHORT_DESCRIPTION, tr("Duplicate this layer")); putValue("help", HelpUtil.ht("/Dialog/LayerList#DuplicateLayer")); updateEnabledState(); } private void duplicate(Layer layer) { if (Main.map == null || Main.map.mapView == null) return; List layerNames = new ArrayList(); for (Layer l: Main.map.mapView.getAllLayers()) { layerNames.add(l.getName()); } if (layer instanceof OsmDataLayer) { OsmDataLayer oldLayer = (OsmDataLayer)layer; // Translators: "Copy of {layer name}" String newName = tr("Copy of {0}", oldLayer.getName()); int i = 2; while (layerNames.contains(newName)) { // Translators: "Copy {number} of {layer name}" newName = tr("Copy {1} of {0}", oldLayer.getName(), i); i++; } Main.main.addLayer(new OsmDataLayer(oldLayer.data.clone(), newName, null)); } } @Override public void actionPerformed(ActionEvent e) { if (layer != null) { duplicate(layer); } else { duplicate(getModel().getSelectedLayers().get(0)); } } protected boolean isActiveLayer(Layer layer) { if (Main.map == null || Main.map.mapView == null) return false; return Main.map.mapView.getActiveLayer() == layer; } @Override public void updateEnabledState() { if (layer == null) { if (getModel().getSelectedLayers().size() == 1) { setEnabled(getModel().getSelectedLayers().get(0) instanceof OsmDataLayer); } else { setEnabled(false); } } else { setEnabled(layer instanceof OsmDataLayer); } } } private static class ActiveLayerCheckBox extends JCheckBox { public ActiveLayerCheckBox() { setHorizontalAlignment(javax.swing.SwingConstants.CENTER); ImageIcon blank = ImageProvider.get("dialogs/layerlist", "blank"); ImageIcon active = ImageProvider.get("dialogs/layerlist", "active"); setIcon(blank); setSelectedIcon(active); setRolloverIcon(blank); setRolloverSelectedIcon(active); setPressedIcon(ImageProvider.get("dialogs/layerlist", "active-pressed")); } } private static class LayerVisibleCheckBox extends JCheckBox { private final ImageIcon icon_eye; private final ImageIcon icon_eye_translucent; private boolean isTranslucent; public LayerVisibleCheckBox() { setHorizontalAlignment(javax.swing.SwingConstants.RIGHT); icon_eye = ImageProvider.get("dialogs/layerlist", "eye"); icon_eye_translucent = ImageProvider.get("dialogs/layerlist", "eye-translucent"); setIcon(ImageProvider.get("dialogs/layerlist", "eye-off")); setPressedIcon(ImageProvider.get("dialogs/layerlist", "eye-pressed")); setSelectedIcon(icon_eye); isTranslucent = false; } public void setTranslucent(boolean isTranslucent) { if (this.isTranslucent == isTranslucent) return; if (isTranslucent) { setSelectedIcon(icon_eye_translucent); } else { setSelectedIcon(icon_eye); } this.isTranslucent = isTranslucent; } public void updateStatus(Layer layer) { boolean visible = layer.isVisible(); setSelected(visible); setTranslucent(layer.getOpacity()<1.0); setToolTipText(visible ? tr("layer is currently visible (click to hide layer)") : tr("layer is currently hidden (click to show layer)")); } } private static class ActiveLayerCellRenderer implements TableCellRenderer { JCheckBox cb; public ActiveLayerCellRenderer() { cb = new ActiveLayerCheckBox(); } @Override public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { boolean active = value != null && (Boolean) value; cb.setSelected(active); cb.setToolTipText(active ? tr("this layer is the active layer") : tr("this layer is not currently active (click to activate)")); return cb; } } private static class LayerVisibleCellRenderer implements TableCellRenderer { LayerVisibleCheckBox cb; public LayerVisibleCellRenderer() { this.cb = new LayerVisibleCheckBox(); } @Override public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { if (value != null) { cb.updateStatus((Layer)value); } return cb; } } private static class LayerVisibleCellEditor extends DefaultCellEditor { LayerVisibleCheckBox cb; public LayerVisibleCellEditor(LayerVisibleCheckBox cb) { super(cb); this.cb = cb; } @Override public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) { cb.updateStatus((Layer)value); return cb; } } private class LayerNameCellRenderer extends DefaultTableCellRenderer { protected boolean isActiveLayer(Layer layer) { if (Main.map == null) return false; if (Main.map.mapView == null) return false; return Main.map.mapView.getActiveLayer() == layer; } @Override public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { if (value == null) return this; Layer layer = (Layer)value; JLabel label = (JLabel)super.getTableCellRendererComponent(table, layer.getName(), isSelected, hasFocus, row, column); if (isActiveLayer(layer)) { label.setFont(label.getFont().deriveFont(Font.BOLD)); } if(Main.pref.getBoolean("dialog.layer.colorname", true)) { Color c = layer.getColor(false); if(c != null) { Color oc = null; for(Layer l : model.getLayers()) { oc = l.getColor(false); if(oc != null) { if(oc.equals(c)) { oc = null; } else { break; } } } /* not more than one color, don't use coloring */ if(oc == null) { c = null; } } if(c == null) { c = Main.pref.getUIColor(isSelected ? "Table.selectionForeground" : "Table.foreground"); } label.setForeground(c); } label.setIcon(layer.getIcon()); label.setToolTipText(layer.getToolTipText()); return label; } } private static class LayerNameCellEditor extends DefaultCellEditor { public LayerNameCellEditor(JosmTextField tf) { super(tf); } @Override public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) { JosmTextField tf = (JosmTextField) super.getTableCellEditorComponent(table, value, isSelected, row, column); tf.setText(value == null ? "" : ((Layer) value).getName()); return tf; } } class PopupMenuHandler extends PopupMenuLauncher { @Override public void launch(MouseEvent evt) { Layer layer = getModel().getLayer(layerList.getSelectedRow()); menu = new LayerListPopup(getModel().getSelectedLayers(), layer); super.launch(evt); } } /** * The action to move up the currently selected entries in the list. */ class MoveUpAction extends AbstractAction implements IEnabledStateUpdating{ public MoveUpAction() { putValue(NAME, tr("Move up")); putValue(SMALL_ICON, ImageProvider.get("dialogs", "up")); putValue(SHORT_DESCRIPTION, tr("Move the selected layer one row up.")); updateEnabledState(); } @Override public void updateEnabledState() { setEnabled(model.canMoveUp()); } @Override public void actionPerformed(ActionEvent e) { model.moveUp(); } } /** * The action to move down the currently selected entries in the list. */ class MoveDownAction extends AbstractAction implements IEnabledStateUpdating { public MoveDownAction() { putValue(NAME, tr("Move down")); putValue(SMALL_ICON, ImageProvider.get("dialogs", "down")); putValue(SHORT_DESCRIPTION, tr("Move the selected layer one row down.")); updateEnabledState(); } @Override public void updateEnabledState() { setEnabled(model.canMoveDown()); } @Override public void actionPerformed(ActionEvent e) { model.moveDown(); } } /** * Observer interface to be implemented by views using {@link LayerListModel} * */ public interface LayerListModelListener { public void makeVisible(int index, Layer layer); public void refresh(); } /** * The layer list model. The model manages a list of layers and provides methods for * moving layers up and down, for toggling their visibility, and for activating a layer. * * The model is a {@link TableModel} and it provides a {@link ListSelectionModel}. It expects * to be configured with a {@link DefaultListSelectionModel}. The selection model is used * to update the selection state of views depending on messages sent to the model. * * The model manages a list of {@link LayerListModelListener} which are mainly notified if * the model requires views to make a specific list entry visible. * * It also listens to {@link PropertyChangeEvent}s of every {@link Layer} it manages, in particular to * the properties {@link Layer#VISIBLE_PROP} and {@link Layer#NAME_PROP}. */ public class LayerListModel extends AbstractTableModel implements MapView.LayerChangeListener, PropertyChangeListener { /** manages list selection state*/ private DefaultListSelectionModel selectionModel; private CopyOnWriteArrayList listeners; /** * constructor * * @param selectionModel the list selection model */ private LayerListModel(DefaultListSelectionModel selectionModel) { this.selectionModel = selectionModel; listeners = new CopyOnWriteArrayList(); } /** * Adds a listener to this model * * @param listener the listener */ public void addLayerListModelListener(LayerListModelListener listener) { if (listener != null) { listeners.addIfAbsent(listener); } } /** * removes a listener from this model * @param listener the listener * */ public void removeLayerListModelListener(LayerListModelListener listener) { listeners.remove(listener); } /** * Fires a make visible event to listeners * * @param index the index of the row to make visible * @param layer the layer at this index * @see LayerListModelListener#makeVisible(int, Layer) */ protected void fireMakeVisible(int index, Layer layer) { for (LayerListModelListener listener : listeners) { listener.makeVisible(index, layer); } } /** * Fires a refresh event to listeners of this model * * @see LayerListModelListener#refresh() */ protected void fireRefresh() { for (LayerListModelListener listener : listeners) { listener.refresh(); } } /** * Populates the model with the current layers managed by * {@link MapView}. * */ public void populate() { for (Layer layer: getLayers()) { // make sure the model is registered exactly once // layer.removePropertyChangeListener(this); layer.addPropertyChangeListener(this); } fireTableDataChanged(); } /** * Marks layer as selected layer. Ignored, if * layer is null. * * @param layer the layer. */ public void setSelectedLayer(Layer layer) { if (layer == null) return; int idx = getLayers().indexOf(layer); if (idx >= 0) { selectionModel.setSelectionInterval(idx, idx); } ensureSelectedIsVisible(); } /** * Replies the list of currently selected layers. Never null, but may * be empty. * * @return the list of currently selected layers. Never null, but may * be empty. */ public List getSelectedLayers() { ArrayList selected = new ArrayList(); for (int i=0; i getSelectedRows() { ArrayList selected = new ArrayList(); for (int i=0; i rows = getSelectedRows(); GuiHelper.runInEDTAndWait(new Runnable() { @Override public void run() { if (rows.isEmpty() && size > 0) { selectionModel.setSelectionInterval(size-1, size-1); } fireTableDataChanged(); fireRefresh(); ensureActiveSelected(); } }); } /** * Invoked when a layer managed by {@link MapView} is added * * @param layer the layer */ protected void onAddLayer(Layer layer) { if (layer == null) return; layer.addPropertyChangeListener(this); fireTableDataChanged(); int idx = getLayers().indexOf(layer); layerList.setRowHeight(idx, Math.max(16, layer.getIcon().getIconHeight())); selectionModel.setSelectionInterval(idx, idx); ensureSelectedIsVisible(); } /** * Replies the first layer. Null if no layers are present * * @return the first layer. Null if no layers are present */ public Layer getFirstLayer() { if (getRowCount() == 0) return null; return getLayers().get(0); } /** * Replies the layer at position index * * @param index the index * @return the layer at position index. Null, * if index is out of range. */ public Layer getLayer(int index) { if (index < 0 || index >= getRowCount()) return null; return getLayers().get(index); } /** * Replies true if the currently selected layers can move up * by one position * * @return true if the currently selected layers can move up * by one position */ public boolean canMoveUp() { List sel = getSelectedRows(); return !sel.isEmpty() && sel.get(0) > 0; } /** * Move up the currently selected layers by one position * */ public void moveUp() { if (!canMoveUp()) return; List sel = getSelectedRows(); for (int row : sel) { Layer l1 = getLayers().get(row); Layer l2 = getLayers().get(row-1); Main.map.mapView.moveLayer(l2,row); Main.map.mapView.moveLayer(l1, row-1); } fireTableDataChanged(); selectionModel.clearSelection(); for (int row : sel) { selectionModel.addSelectionInterval(row-1, row-1); } ensureSelectedIsVisible(); } /** * Replies true if the currently selected layers can move down * by one position * * @return true if the currently selected layers can move down * by one position */ public boolean canMoveDown() { List sel = getSelectedRows(); return !sel.isEmpty() && sel.get(sel.size()-1) < getLayers().size()-1; } /** * Move down the currently selected layers by one position * */ public void moveDown() { if (!canMoveDown()) return; List sel = getSelectedRows(); Collections.reverse(sel); for (int row : sel) { Layer l1 = getLayers().get(row); Layer l2 = getLayers().get(row+1); Main.map.mapView.moveLayer(l1, row+1); Main.map.mapView.moveLayer(l2, row); } fireTableDataChanged(); selectionModel.clearSelection(); for (int row : sel) { selectionModel.addSelectionInterval(row+1, row+1); } ensureSelectedIsVisible(); } /** * Make sure the first of the selected layers is visible in the * views of this model. * */ protected void ensureSelectedIsVisible() { int index = selectionModel.getMinSelectionIndex(); if (index < 0) return; if (index >= getLayers().size()) return; Layer layer = getLayers().get(index); fireMakeVisible(index, layer); } /** * Replies a list of layers which are possible merge targets * for source * * @param source the source layer * @return a list of layers which are possible merge targets * for source. Never null, but can be empty. */ public List getPossibleMergeTargets(Layer source) { ArrayList targets = new ArrayList(); if (source == null) return targets; for (Layer target : getLayers()) { if (source == target) { continue; } if (target.isMergable(source) && source.isMergable(target)) { targets.add(target); } } return targets; } /** * Replies the list of layers currently managed by {@link MapView}. * Never null, but can be empty. * * @return the list of layers currently managed by {@link MapView}. * Never null, but can be empty. */ public List getLayers() { if (Main.map == null || Main.map.mapView == null) return Collections.emptyList(); return Main.map.mapView.getAllLayersAsList(); } /** * Ensures that at least one layer is selected in the layer dialog * */ protected void ensureActiveSelected() { if (getLayers().isEmpty()) return; final Layer activeLayer = getActiveLayer(); if (activeLayer != null) { // there's an active layer - select it and make it // visible int idx = getLayers().indexOf(activeLayer); selectionModel.setSelectionInterval(idx, idx); ensureSelectedIsVisible(); } else { // no active layer - select the first one and make // it visible selectionModel.setSelectionInterval(0, 0); ensureSelectedIsVisible(); } } /** * Replies the active layer. null, if no active layer is available * * @return the active layer. null, if no active layer is available */ protected Layer getActiveLayer() { if (Main.map == null || Main.map.mapView == null) return null; return Main.map.mapView.getActiveLayer(); } /* ------------------------------------------------------------------------------ */ /* Interface TableModel */ /* ------------------------------------------------------------------------------ */ @Override public int getRowCount() { List layers = getLayers(); if (layers == null) return 0; return layers.size(); } @Override public int getColumnCount() { return 3; } @Override public Object getValueAt(int row, int col) { if (row >= 0 && row < getLayers().size()) { switch (col) { case 0: return getLayers().get(row) == getActiveLayer(); case 1: return getLayers().get(row); case 2: return getLayers().get(row); default: throw new RuntimeException(); } } return null; } @Override public boolean isCellEditable(int row, int col) { if (col == 0 && getActiveLayer() == getLayers().get(row)) return false; return true; } @Override public void setValueAt(Object value, int row, int col) { Layer l = getLayers().get(row); switch (col) { case 0: Main.map.mapView.setActiveLayer(l); l.setVisible(true); break; case 1: l.setVisible((Boolean) value); break; case 2: l.setName((String) value); break; default: throw new RuntimeException(); } fireTableCellUpdated(row, col); } /* ------------------------------------------------------------------------------ */ /* Interface LayerChangeListener */ /* ------------------------------------------------------------------------------ */ @Override public void activeLayerChange(final Layer oldLayer, final Layer newLayer) { GuiHelper.runInEDTAndWait(new Runnable() { @Override public void run() { if (oldLayer != null) { int idx = getLayers().indexOf(oldLayer); if (idx >= 0) { fireTableRowsUpdated(idx,idx); } } if (newLayer != null) { int idx = getLayers().indexOf(newLayer); if (idx >= 0) { fireTableRowsUpdated(idx,idx); } } ensureActiveSelected(); } }); } @Override public void layerAdded(Layer newLayer) { onAddLayer(newLayer); } @Override public void layerRemoved(final Layer oldLayer) { onRemoveLayer(oldLayer); } /* ------------------------------------------------------------------------------ */ /* Interface PropertyChangeListener */ /* ------------------------------------------------------------------------------ */ @Override public void propertyChange(PropertyChangeEvent evt) { if (evt.getSource() instanceof Layer) { Layer layer = (Layer)evt.getSource(); final int idx = getLayers().indexOf(layer); if (idx < 0) return; fireRefresh(); } } } static class LayerList extends JTable { public LayerList(TableModel dataModel) { super(dataModel); } public void scrollToVisible(int row, int col) { if (!(getParent() instanceof JViewport)) return; JViewport viewport = (JViewport) getParent(); Rectangle rect = getCellRect(row, col, true); Point pt = viewport.getViewPosition(); rect.setLocation(rect.x - pt.x, rect.y - pt.y); viewport.scrollRectToVisible(rect); } } /** * Creates a {@link ShowHideLayerAction} for layer in the * context of this {@link LayerListDialog}. * * @return the action */ public ShowHideLayerAction createShowHideLayerAction() { ShowHideLayerAction act = new ShowHideLayerAction(true); act.putValue(Action.NAME, tr("Show/Hide")); return act; } /** * Creates a {@link DeleteLayerAction} for layer in the * context of this {@link LayerListDialog}. * * @return the action */ public DeleteLayerAction createDeleteLayerAction() { // the delete layer action doesn't depend on the current layer return new DeleteLayerAction(); } /** * Creates a {@link ActivateLayerAction} for layer in the * context of this {@link LayerListDialog}. * * @param layer the layer * @return the action */ public ActivateLayerAction createActivateLayerAction(Layer layer) { return new ActivateLayerAction(layer); } /** * Creates a {@link MergeLayerAction} for layer in the * context of this {@link LayerListDialog}. * * @param layer the layer * @return the action */ public MergeAction createMergeLayerAction(Layer layer) { return new MergeAction(layer); } public static Layer getLayerForIndex(int index) { if (!Main.isDisplayingMapView()) return null; List layers = Main.map.mapView.getAllLayersAsList(); if (index < layers.size() && index >= 0) return layers.get(index); else return null; } // This is not Class on purpose, to allow asking for layers implementing some interface public static List getLayerInfoByClass(Class layerClass) { List result = new ArrayList(); if (!Main.isDisplayingMapView()) return result; List layers = Main.map.mapView.getAllLayersAsList(); int index = 0; for (Layer l: layers) { if (layerClass.isAssignableFrom(l.getClass())) { result.add(new MultikeyInfo(index, l.getName())); } index++; } return result; } public static boolean isLayerValid(Layer l) { if (l == null) return false; if (!Main.isDisplayingMapView()) return false; return Main.map.mapView.getAllLayersAsList().contains(l); } public static MultikeyInfo getLayerInfo(Layer l) { if (l == null) return null; if (!Main.isDisplayingMapView()) return null; int index = Main.map.mapView.getAllLayersAsList().indexOf(l); if (index < 0) return null; return new MultikeyInfo(index, l.getName()); } }