// 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.GraphicsEnvironment;
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.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.CopyOnWriteArrayList;
import javax.swing.AbstractAction;
import javax.swing.DefaultCellEditor;
import javax.swing.DefaultListSelectionModel;
import javax.swing.DropMode;
import javax.swing.ImageIcon;
import javax.swing.JCheckBox;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JTable;
import javax.swing.KeyStroke;
import javax.swing.ListSelectionModel;
import javax.swing.UIManager;
import javax.swing.event.ListDataEvent;
import javax.swing.event.ListSelectionEvent;
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.data.preferences.AbstractProperty;
import org.openstreetmap.josm.gui.MapView;
import org.openstreetmap.josm.gui.SideButton;
import org.openstreetmap.josm.gui.dialogs.layer.ActivateLayerAction;
import org.openstreetmap.josm.gui.dialogs.layer.DeleteLayerAction;
import org.openstreetmap.josm.gui.dialogs.layer.DuplicateAction;
import org.openstreetmap.josm.gui.dialogs.layer.LayerListTransferHandler;
import org.openstreetmap.josm.gui.dialogs.layer.LayerVisibilityAction;
import org.openstreetmap.josm.gui.dialogs.layer.MergeAction;
import org.openstreetmap.josm.gui.dialogs.layer.MoveDownAction;
import org.openstreetmap.josm.gui.dialogs.layer.MoveUpAction;
import org.openstreetmap.josm.gui.dialogs.layer.ShowHideLayerAction;
import org.openstreetmap.josm.gui.layer.JumpToMarkerActions;
import org.openstreetmap.josm.gui.layer.Layer;
import org.openstreetmap.josm.gui.layer.LayerManager.LayerAddEvent;
import org.openstreetmap.josm.gui.layer.LayerManager.LayerChangeListener;
import org.openstreetmap.josm.gui.layer.LayerManager.LayerOrderChangeEvent;
import org.openstreetmap.josm.gui.layer.LayerManager.LayerRemoveEvent;
import org.openstreetmap.josm.gui.layer.MainLayerManager;
import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeEvent;
import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeListener;
import org.openstreetmap.josm.gui.layer.NativeScaleLayer;
import org.openstreetmap.josm.gui.util.GuiHelper;
import org.openstreetmap.josm.gui.widgets.DisableShortcutsOnFocusGainedTextField;
import org.openstreetmap.josm.gui.widgets.JosmTextField;
import org.openstreetmap.josm.gui.widgets.PopupMenuLauncher;
import org.openstreetmap.josm.gui.widgets.ScrollableTable;
import org.openstreetmap.josm.tools.ImageProvider;
import org.openstreetmap.josm.tools.InputMapUtils;
import org.openstreetmap.josm.tools.MultikeyActionsHandler;
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.
* <p>
* Support for multiple {@link LayerListDialog} is currently not complete but intended for the future.
* @since 17
*/
public class LayerListDialog extends ToggleDialog {
/** the unique instance of the dialog */
private static volatile LayerListDialog instance;
/**
* Creates the instance of the dialog. It's connected to the layer manager
*
* @param layerManager the layer manager
* @since 11885 (signature)
*/
public static void createInstance(MainLayerManager layerManager) {
if (instance != null)
throw new IllegalStateException("Dialog was already created");
instance = new LayerListDialog(layerManager);
}
/**
* Replies the instance of the dialog
*
* @return the instance of the dialog
* @throws IllegalStateException if the dialog is not created yet
* @see #createInstance(MainLayerManager)
*/
public static LayerListDialog getInstance() {
if (instance == null)
throw new IllegalStateException("Dialog not created yet. Invoke createInstance() first");
return instance;
}
/** the model for the layer list */
private final LayerListModel model;
/** the list of layers (technically its a JTable, but appears like a list) */
private final LayerList layerList;
private final ActivateLayerAction activateLayerAction;
private final 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 {
private final int layerIndex;
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 transient Shortcut[] visibilityToggleShortcuts = new Shortcut[10];
private final ToggleLayerIndexVisibility[] visibilityToggleActions = new ToggleLayerIndexVisibility[10];
/**
* The {@link MainLayerManager} this list is for.
*/
private final transient MainLayerManager layerManager;
/**
* registers (shortcut to toggle right hand side toggle dialogs)+(number keys) shortcuts
* to toggle the visibility of the first ten layers.
*/
private void createVisibilityToggleShortcuts() {
for (int i = 0; i < 10; i++) {
final int i1 = i + 1;
/* POSSIBLE SHORTCUTS: 1,2,3,4,5,6,7,8,9,0=10 */
visibilityToggleShortcuts[i] = Shortcut.registerShortcut("subwindow:layers:toggleLayer" + i1,
tr("Toggle visibility of layer: {0}", i1), KeyEvent.VK_0 + (i1 % 10), Shortcut.ALT);
visibilityToggleActions[i] = new ToggleLayerIndexVisibility(i);
Main.registerActionShortcut(visibilityToggleActions[i], visibilityToggleShortcuts[i]);
}
}
/**
* Creates a layer list and attach it to the given layer manager.
* @param layerManager The layer manager this list is for
* @since 10467
*/
public LayerListDialog(MainLayerManager layerManager) {
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);
this.layerManager = layerManager;
// create the models
//
DefaultListSelectionModel selectionModel = new DefaultListSelectionModel();
selectionModel.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
model = new LayerListModel(layerManager, 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", Boolean.TRUE);
layerList.putClientProperty("JTable.autoStartsEdit", Boolean.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 NativeScaleLayerCellRenderer());
layerList.getColumnModel().getColumn(1).setCellEditor(new DefaultCellEditor(new NativeScaleLayerCheckBox()));
layerList.getColumnModel().getColumn(1).setMaxWidth(12);
layerList.getColumnModel().getColumn(1).setPreferredWidth(12);
layerList.getColumnModel().getColumn(1).setResizable(false);
layerList.getColumnModel().getColumn(2).setCellRenderer(new LayerVisibleCellRenderer());
layerList.getColumnModel().getColumn(2).setCellEditor(new LayerVisibleCellEditor(new LayerVisibleCheckBox()));
layerList.getColumnModel().getColumn(2).setMaxWidth(16);
layerList.getColumnModel().getColumn(2).setPreferredWidth(16);
layerList.getColumnModel().getColumn(2).setResizable(false);
layerList.getColumnModel().getColumn(3).setCellRenderer(new LayerNameCellRenderer());
layerList.getColumnModel().getColumn(3).setCellEditor(new LayerNameCellEditor(new DisableShortcutsOnFocusGainedTextField()));
// Disable some default JTable shortcuts to use JOSM ones (see #5678, #10458)
for (KeyStroke ks : new KeyStroke[] {
KeyStroke.getKeyStroke(KeyEvent.VK_C, GuiHelper.getMenuShortcutKeyMaskEx()),
KeyStroke.getKeyStroke(KeyEvent.VK_V, GuiHelper.getMenuShortcutKeyMaskEx()),
KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, InputEvent.SHIFT_DOWN_MASK),
KeyStroke.getKeyStroke(KeyEvent.VK_UP, InputEvent.SHIFT_DOWN_MASK),
KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, InputEvent.SHIFT_DOWN_MASK),
KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, InputEvent.SHIFT_DOWN_MASK),
KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, InputEvent.CTRL_DOWN_MASK),
KeyStroke.getKeyStroke(KeyEvent.VK_UP, InputEvent.CTRL_DOWN_MASK),
KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, InputEvent.CTRL_DOWN_MASK),
KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, InputEvent.CTRL_DOWN_MASK),
KeyStroke.getKeyStroke(KeyEvent.VK_PAGE_UP, 0),
KeyStroke.getKeyStroke(KeyEvent.VK_PAGE_DOWN, 0),
KeyStroke.getKeyStroke(KeyEvent.VK_TAB, 0),
KeyStroke.getKeyStroke(KeyEvent.VK_F8, 0),
}) {
layerList.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(ks, new Object());
}
// init the model
//
model.populate();
model.setSelectedLayer(layerManager.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(model);
adaptTo(moveUpAction, model);
adaptTo(moveUpAction, selectionModel);
// -- move down action
MoveDownAction moveDownAction = new MoveDownAction(model);
adaptTo(moveDownAction, model);
adaptTo(moveDownAction, selectionModel);
// -- activate action
activateLayerAction = new ActivateLayerAction(model);
activateLayerAction.updateEnabledState();
MultikeyActionsHandler.getInstance().addAction(activateLayerAction);
adaptTo(activateLayerAction, selectionModel);
JumpToMarkerActions.initialize();
// -- show hide action
showHideLayerAction = new ShowHideLayerAction(model);
MultikeyActionsHandler.getInstance().addAction(showHideLayerAction);
adaptTo(showHideLayerAction, selectionModel);
LayerVisibilityAction visibilityAction = new LayerVisibilityAction(model);
adaptTo(visibilityAction, selectionModel);
SideButton visibilityButton = new SideButton(visibilityAction, false);
visibilityAction.setCorrespondingSideButton(visibilityButton);
// -- delete layer action
DeleteLayerAction deleteLayerAction = new DeleteLayerAction(model);
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(moveUpAction, false),
new SideButton(moveDownAction, false),
new SideButton(activateLayerAction, false),
visibilityButton,
new SideButton(deleteLayerAction, false)
));
createVisibilityToggleShortcuts();
}
/**
* Gets the layer manager this dialog is for.
* @return The layer manager.
* @since 10288
*/
public MainLayerManager getLayerManager() {
return layerManager;
}
@Override
public void showNotify() {
layerManager.addActiveLayerChangeListener(activateLayerAction);
layerManager.addLayerChangeListener(model, true);
layerManager.addAndFireActiveLayerChangeListener(model);
model.populate();
}
@Override
public void hideNotify() {
layerManager.removeLayerChangeListener(model, true);
layerManager.removeActiveLayerChangeListener(model);
layerManager.removeActiveLayerChangeListener(activateLayerAction);
}
/**
* Returns the layer list model.
* @return the layer list model
*/
public LayerListModel getModel() {
return model;
}
/**
* Wires <code>listener</code> to <code>listSelectionModel</code> in such a way, that
* <code>listener</code> 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(e -> listener.updateEnabledState());
}
/**
* Wires <code>listener</code> to <code>listModel</code> in such a way, that
* <code>listener</code> 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(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;
}
private static class ActiveLayerCheckBox extends JCheckBox {
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 iconEye;
private final ImageIcon iconEyeTranslucent;
private boolean isTranslucent;
/**
* Constructs a new {@code LayerVisibleCheckBox}.
*/
LayerVisibleCheckBox() {
setHorizontalAlignment(javax.swing.SwingConstants.RIGHT);
iconEye = ImageProvider.get("dialogs/layerlist", "eye");
iconEyeTranslucent = ImageProvider.get("dialogs/layerlist", "eye-translucent");
setIcon(ImageProvider.get("dialogs/layerlist", "eye-off"));
setPressedIcon(ImageProvider.get("dialogs/layerlist", "eye-pressed"));
setSelectedIcon(iconEye);
isTranslucent = false;
}
public void setTranslucent(boolean isTranslucent) {
if (this.isTranslucent == isTranslucent) return;
if (isTranslucent) {
setSelectedIcon(iconEyeTranslucent);
} else {
setSelectedIcon(iconEye);
}
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 NativeScaleLayerCheckBox extends JCheckBox {
NativeScaleLayerCheckBox() {
setHorizontalAlignment(javax.swing.SwingConstants.CENTER);
ImageIcon blank = ImageProvider.get("dialogs/layerlist", "blank");
ImageIcon active = ImageProvider.get("dialogs/layerlist", "scale");
setIcon(blank);
setSelectedIcon(active);
}
}
private static class ActiveLayerCellRenderer implements TableCellRenderer {
private final JCheckBox cb;
/**
* Constructs a new {@code ActiveLayerCellRenderer}.
*/
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 {
private final LayerVisibleCheckBox cb;
/**
* Constructs a new {@code LayerVisibleCellRenderer}.
*/
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 {
private final LayerVisibleCheckBox cb;
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 static class NativeScaleLayerCellRenderer implements TableCellRenderer {
private final JCheckBox cb;
/**
* Constructs a new {@code ActiveLayerCellRenderer}.
*/
NativeScaleLayerCellRenderer() {
cb = new NativeScaleLayerCheckBox();
}
@Override
public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
Layer layer = (Layer) value;
if (layer instanceof NativeScaleLayer) {
boolean active = ((NativeScaleLayer) layer) == Main.map.mapView.getNativeScaleLayer();
cb.setSelected(active);
cb.setToolTipText(active
? tr("scale follows native resolution of this layer")
: tr("scale follows native resolution of another layer (click to set this layer)")
);
} else {
cb.setSelected(false);
cb.setToolTipText(tr("this layer has no native resolution"));
}
return cb;
}
}
private class LayerNameCellRenderer extends DefaultTableCellRenderer {
protected boolean isActiveLayer(Layer layer) {
return getLayerManager().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)) {
AbstractProperty<Color> prop = layer.getColorProperty();
Color c = prop == null ? null : prop.get();
if (c == null || !model.getLayers().stream()
.map(Layer::getColorProperty)
.filter(Objects::nonNull)
.map(AbstractProperty::get)
.anyMatch(oc -> oc != null && !oc.equals(c))) {
/* not more than one color, don't use coloring */
label.setForeground(UIManager.getColor(isSelected ? "Table.selectionForeground" : "Table.foreground"));
} else {
label.setForeground(c);
}
}
label.setIcon(layer.getIcon());
label.setToolTipText(layer.getToolTipText());
return label;
}
}
private static class LayerNameCellEditor extends DefaultCellEditor {
LayerNameCellEditor(DisableShortcutsOnFocusGainedTextField 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 showMenu(MouseEvent evt) {
menu = new LayerListPopup(getModel().getSelectedLayers());
super.showMenu(evt);
}
}
/**
* Observer interface to be implemented by views using {@link LayerListModel}.
*/
public interface LayerListModelListener {
/**
* Fired when a layer is made visible.
* @param index the layer index
* @param layer the layer
*/
void makeVisible(int index, Layer layer);
/**
* Fired when something has changed in the layer list model.
*/
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 static final class LayerListModel extends AbstractTableModel
implements LayerChangeListener, ActiveLayerChangeListener, PropertyChangeListener {
/** manages list selection state*/
private final DefaultListSelectionModel selectionModel;
private final CopyOnWriteArrayList<LayerListModelListener> listeners;
private LayerList layerList;
private final MainLayerManager layerManager;
/**
* constructor
* @param layerManager The layer manager to use for the list.
* @param selectionModel the list selection model
*/
LayerListModel(MainLayerManager layerManager, DefaultListSelectionModel selectionModel) {
this.layerManager = layerManager;
this.selectionModel = selectionModel;
listeners = new CopyOnWriteArrayList<>();
}
void setLayerList(LayerList layerList) {
this.layerList = layerList;
}
/**
* The layer manager this model is for.
* @return The layer manager.
*/
public MainLayerManager getLayerManager() {
return layerManager;
}
/**
* 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)
*/
private 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()
*/
private 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 <code>layer</code> 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<Layer> getSelectedLayers() {
List<Layer> selected = new ArrayList<>();
List<Layer> layers = getLayers();
for (int i = 0; i < layers.size(); i++) {
if (selectionModel.isSelectedIndex(i)) {
selected.add(layers.get(i));
}
}
return selected;
}
/**
* Replies a the list of indices of the selected rows. Never null, but may be empty.
*
* @return the list of indices of the selected rows. Never null, but may be empty.
*/
public List<Integer> getSelectedRows() {
List<Integer> selected = new ArrayList<>();
for (int i = 0; i < getLayers().size(); i++) {
if (selectionModel.isSelectedIndex(i)) {
selected.add(i);
}
}
return selected;
}
/**
* Invoked if a layer managed by {@link MapView} is removed
*
* @param layer the layer which is removed
*/
private void onRemoveLayer(Layer layer) {
if (layer == null)
return;
layer.removePropertyChangeListener(this);
final int size = getRowCount();
final List<Integer> rows = getSelectedRows();
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
*/
private void onAddLayer(Layer layer) {
if (layer == null)
return;
layer.addPropertyChangeListener(this);
fireTableDataChanged();
int idx = getLayers().indexOf(layer);
if (layerList != null) {
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 <code>index</code>
*
* @param index the index
* @return the layer at position <code>index</code>. 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<Integer> 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<Integer> sel = getSelectedRows();
List<Layer> layers = getLayers();
for (int row : sel) {
Layer l1 = layers.get(row);
Layer l2 = layers.get(row-1);
Main.map.mapView.moveLayer(l2, row);
Main.map.mapView.moveLayer(l1, row-1);
}
fireTableDataChanged();
selectionModel.setValueIsAdjusting(true);
selectionModel.clearSelection();
for (int row : sel) {
selectionModel.addSelectionInterval(row-1, row-1);
}
selectionModel.setValueIsAdjusting(false);
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<Integer> 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<Integer> sel = getSelectedRows();
Collections.reverse(sel);
List<Layer> layers = getLayers();
for (int row : sel) {
Layer l1 = layers.get(row);
Layer l2 = layers.get(row+1);
Main.map.mapView.moveLayer(l1, row+1);
Main.map.mapView.moveLayer(l2, row);
}
fireTableDataChanged();
selectionModel.setValueIsAdjusting(true);
selectionModel.clearSelection();
for (int row : sel) {
selectionModel.addSelectionInterval(row+1, row+1);
}
selectionModel.setValueIsAdjusting(false);
ensureSelectedIsVisible();
}
/**
* Make sure the first of the selected layers is visible in the views of this model.
*/
private void ensureSelectedIsVisible() {
int index = selectionModel.getMinSelectionIndex();
if (index < 0)
return;
List<Layer> layers = getLayers();
if (index >= layers.size())
return;
Layer layer = layers.get(index);
fireMakeVisible(index, layer);
}
/**
* Replies a list of layers which are possible merge targets for <code>source</code>
*
* @param source the source layer
* @return a list of layers which are possible merge targets
* for <code>source</code>. Never null, but can be empty.
*/
public List<Layer> getPossibleMergeTargets(Layer source) {
List<Layer> 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<Layer> getLayers() {
return getLayerManager().getLayers();
}
/**
* Ensures that at least one layer is selected in the layer dialog
*
*/
private void ensureActiveSelected() {
List<Layer> layers = getLayers();
if (layers.isEmpty())
return;
final Layer activeLayer = getActiveLayer();
if (activeLayer != null) {
// there's an active layer - select it and make it visible
int idx = layers.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
*/
private Layer getActiveLayer() {
return getLayerManager().getActiveLayer();
}
/* ------------------------------------------------------------------------------ */
/* Interface TableModel */
/* ------------------------------------------------------------------------------ */
@Override
public int getRowCount() {
List<Layer> layers = getLayers();
return layers == null ? 0 : layers.size();
}
@Override
public int getColumnCount() {
return 4;
}
@Override
public Object getValueAt(int row, int col) {
List<Layer> layers = getLayers();
if (row >= 0 && row < layers.size()) {
switch (col) {
case 0: return layers.get(row) == getActiveLayer();
case 1:
case 2:
case 3: return layers.get(row);
default: // Do nothing
}
}
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) {
List<Layer> layers = getLayers();
if (row < layers.size()) {
Layer l = layers.get(row);
switch (col) {
case 0:
getLayerManager().setActiveLayer(l);
l.setVisible(true);
break;
case 1:
NativeScaleLayer oldLayer = Main.map.mapView.getNativeScaleLayer();
if (oldLayer == l) {
Main.map.mapView.setNativeScaleLayer(null);
} else if (l instanceof NativeScaleLayer) {
Main.map.mapView.setNativeScaleLayer((NativeScaleLayer) l);
if (oldLayer != null) {
int idx = getLayers().indexOf(oldLayer);
if (idx >= 0) {
fireTableCellUpdated(idx, col);
}
}
}
break;
case 2:
l.setVisible((Boolean) value);
break;
case 3:
l.rename((String) value);
break;
default:
throw new IllegalArgumentException("Wrong column: " + col);
}
fireTableCellUpdated(row, col);
}
}
/* ------------------------------------------------------------------------------ */
/* Interface ActiveLayerChangeListener */
/* ------------------------------------------------------------------------------ */
@Override
public void activeOrEditLayerChanged(ActiveLayerChangeEvent e) {
Layer oldLayer = e.getPreviousActiveLayer();
if (oldLayer != null) {
int idx = getLayers().indexOf(oldLayer);
if (idx >= 0) {
fireTableRowsUpdated(idx, idx);
}
}
Layer newLayer = getActiveLayer();
if (newLayer != null) {
int idx = getLayers().indexOf(newLayer);
if (idx >= 0) {
fireTableRowsUpdated(idx, idx);
}
}
ensureActiveSelected();
}
/* ------------------------------------------------------------------------------ */
/* Interface LayerChangeListener */
/* ------------------------------------------------------------------------------ */
@Override
public void layerAdded(LayerAddEvent e) {
onAddLayer(e.getAddedLayer());
}
@Override
public void layerRemoving(LayerRemoveEvent e) {
onRemoveLayer(e.getRemovedLayer());
}
@Override
public void layerOrderChanged(LayerOrderChangeEvent e) {
fireTableDataChanged();
}
/* ------------------------------------------------------------------------------ */
/* 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();
}
}
}
/**
* This component displays a list of layers and provides the methods needed by {@link LayerListModel}.
*/
static class LayerList extends ScrollableTable {
LayerList(LayerListModel dataModel) {
super(dataModel);
dataModel.setLayerList(this);
if (!GraphicsEnvironment.isHeadless()) {
setDragEnabled(true);
}
setDropMode(DropMode.INSERT_ROWS);
setTransferHandler(new LayerListTransferHandler());
}
@Override
public LayerListModel getModel() {
return (LayerListModel) super.getModel();
}
}
/**
* Creates a {@link ShowHideLayerAction} in the context of this {@link LayerListDialog}.
*
* @return the action
*/
public ShowHideLayerAction createShowHideLayerAction() {
return new ShowHideLayerAction(model);
}
/**
* Creates a {@link DeleteLayerAction} in the context of this {@link LayerListDialog}.
*
* @return the action
*/
public DeleteLayerAction createDeleteLayerAction() {
return new DeleteLayerAction(model);
}
/**
* Creates a {@link ActivateLayerAction} for <code>layer</code> in the context of this {@link LayerListDialog}.
*
* @param layer the layer
* @return the action
*/
public ActivateLayerAction createActivateLayerAction(Layer layer) {
return new ActivateLayerAction(layer, model);
}
/**
* Creates a {@link MergeLayerAction} for <code>layer</code> in the context of this {@link LayerListDialog}.
*
* @param layer the layer
* @return the action
*/
public MergeAction createMergeLayerAction(Layer layer) {
return new MergeAction(layer, model);
}
/**
* Creates a {@link DuplicateAction} for <code>layer</code> in the context of this {@link LayerListDialog}.
*
* @param layer the layer
* @return the action
*/
public DuplicateAction createDuplicateLayerAction(Layer layer) {
return new DuplicateAction(layer, model);
}
/**
* Returns the layer at given index, or {@code null}.
* @param index the index
* @return the layer at given index, or {@code null} if index out of range
*/
public static Layer getLayerForIndex(int index) {
List<Layer> layers = Main.getLayerManager().getLayers();
if (index < layers.size() && index >= 0)
return layers.get(index);
else
return null;
}
/**
* Returns a list of info on all layers of a given class.
* @param layerClass The layer class. This is not {@code Class<? extends Layer>} on purpose,
* to allow asking for layers implementing some interface
* @return list of info on all layers assignable from {@code layerClass}
*/
public static List<MultikeyInfo> getLayerInfoByClass(Class<?> layerClass) {
List<MultikeyInfo> result = new ArrayList<>();
List<Layer> layers = Main.getLayerManager().getLayers();
int index = 0;
for (Layer l: layers) {
if (layerClass.isAssignableFrom(l.getClass())) {
result.add(new MultikeyInfo(index, l.getName()));
}
index++;
}
return result;
}
/**
* Determines if a layer is valid (contained in global layer list).
* @param l the layer
* @return {@code true} if layer {@code l} is contained in current layer list
*/
public static boolean isLayerValid(Layer l) {
if (l == null)
return false;
return Main.getLayerManager().containsLayer(l);
}
/**
* Returns info about layer.
* @param l the layer
* @return info about layer {@code l}
*/
public static MultikeyInfo getLayerInfo(Layer l) {
if (l == null)
return null;
int index = Main.getLayerManager().getLayers().indexOf(l);
if (index < 0)
return null;
return new MultikeyInfo(index, l.getName());
}
}