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