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