// 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. *
* 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.addAndFireLayerChangeListener(model);
layerManager.addAndFireActiveLayerChangeListener(model);
model.populate();
}
@Override
public void hideNotify() {
layerManager.removeAndFireLayerChangeListener(model);
layerManager.removeActiveLayerChangeListener(model);
layerManager.removeActiveLayerChangeListener(activateLayerAction);
}
/**
* Returns the layer list model.
* @return the layer list model
*/
public LayerListModel getModel() {
return model;
}
/**
* 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(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(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)) {
AbstractPropertylayer
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 Listindex
*
* @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() {
Listsource
*
* @param source the source layer
* @return a list of layers which are possible merge targets
* for source
. Never null, but can be empty.
*/
public Listlayer
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 layer
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 layer
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