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