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.table.AbstractTableModel; 037import javax.swing.table.DefaultTableCellRenderer; 038import javax.swing.table.TableCellRenderer; 039import javax.swing.table.TableModel; 040 041import org.openstreetmap.josm.actions.MergeLayerAction; 042import org.openstreetmap.josm.data.coor.EastNorth; 043import org.openstreetmap.josm.data.imagery.OffsetBookmark; 044import org.openstreetmap.josm.data.preferences.AbstractProperty; 045import org.openstreetmap.josm.gui.MainApplication; 046import org.openstreetmap.josm.gui.MapFrame; 047import org.openstreetmap.josm.gui.MapView; 048import org.openstreetmap.josm.gui.SideButton; 049import org.openstreetmap.josm.gui.dialogs.layer.ActivateLayerAction; 050import org.openstreetmap.josm.gui.dialogs.layer.DeleteLayerAction; 051import org.openstreetmap.josm.gui.dialogs.layer.DuplicateAction; 052import org.openstreetmap.josm.gui.dialogs.layer.LayerListTransferHandler; 053import org.openstreetmap.josm.gui.dialogs.layer.LayerVisibilityAction; 054import org.openstreetmap.josm.gui.dialogs.layer.MergeAction; 055import org.openstreetmap.josm.gui.dialogs.layer.MoveDownAction; 056import org.openstreetmap.josm.gui.dialogs.layer.MoveUpAction; 057import org.openstreetmap.josm.gui.dialogs.layer.ShowHideLayerAction; 058import org.openstreetmap.josm.gui.layer.AbstractTileSourceLayer; 059import org.openstreetmap.josm.gui.layer.JumpToMarkerActions; 060import org.openstreetmap.josm.gui.layer.Layer; 061import org.openstreetmap.josm.gui.layer.LayerManager.LayerAddEvent; 062import org.openstreetmap.josm.gui.layer.LayerManager.LayerChangeListener; 063import org.openstreetmap.josm.gui.layer.LayerManager.LayerOrderChangeEvent; 064import org.openstreetmap.josm.gui.layer.LayerManager.LayerRemoveEvent; 065import org.openstreetmap.josm.gui.layer.MainLayerManager; 066import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeEvent; 067import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeListener; 068import org.openstreetmap.josm.gui.layer.NativeScaleLayer; 069import org.openstreetmap.josm.gui.layer.imagery.TileSourceDisplaySettings.DisplaySettingsChangeEvent; 070import org.openstreetmap.josm.gui.layer.imagery.TileSourceDisplaySettings.DisplaySettingsChangeListener; 071import org.openstreetmap.josm.gui.util.MultikeyActionsHandler; 072import org.openstreetmap.josm.gui.util.MultikeyShortcutAction.MultikeyInfo; 073import org.openstreetmap.josm.gui.util.ReorderableTableModel; 074import org.openstreetmap.josm.gui.util.TableHelper; 075import org.openstreetmap.josm.gui.widgets.DisableShortcutsOnFocusGainedTextField; 076import org.openstreetmap.josm.gui.widgets.JosmTextField; 077import org.openstreetmap.josm.gui.widgets.PopupMenuLauncher; 078import org.openstreetmap.josm.gui.widgets.ScrollableTable; 079import org.openstreetmap.josm.spi.preferences.Config; 080import org.openstreetmap.josm.tools.ArrayUtils; 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(48); 224 layerList.getColumnModel().getColumn(3).setPreferredWidth(48); 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 TableHelper.adaptTo(moveUpAction, model); 271 TableHelper.adaptTo(moveUpAction, selectionModel); 272 273 // -- move down action 274 MoveDownAction moveDownAction = new MoveDownAction(model); 275 TableHelper.adaptTo(moveDownAction, model); 276 TableHelper.adaptTo(moveDownAction, selectionModel); 277 278 // -- activate action 279 activateLayerAction = new ActivateLayerAction(model); 280 activateLayerAction.updateEnabledState(); 281 MultikeyActionsHandler.getInstance().addAction(activateLayerAction); 282 TableHelper.adaptTo(activateLayerAction, selectionModel); 283 284 JumpToMarkerActions.initialize(); 285 286 // -- show hide action 287 showHideLayerAction = new ShowHideLayerAction(model); 288 MultikeyActionsHandler.getInstance().addAction(showHideLayerAction); 289 TableHelper.adaptTo(showHideLayerAction, selectionModel); 290 291 LayerVisibilityAction visibilityAction = new LayerVisibilityAction(model); 292 TableHelper.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 TableHelper.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 @Override 361 public void destroy() { 362 for (int i = 0; i < 10; i++) { 363 MainApplication.unregisterActionShortcut(visibilityToggleActions[i], visibilityToggleShortcuts[i]); 364 } 365 MultikeyActionsHandler.getInstance().removeAction(activateLayerAction); 366 MultikeyActionsHandler.getInstance().removeAction(showHideLayerAction); 367 JumpToMarkerActions.unregisterActions(); 368 layerList.setTransferHandler(null); 369 super.destroy(); 370 instance = null; 371 } 372 373 static ImageIcon createBlankIcon() { 374 return ImageProvider.createBlankIcon(ImageSizes.LAYER); 375 } 376 377 private static class ActiveLayerCheckBox extends JCheckBox { 378 ActiveLayerCheckBox() { 379 setHorizontalAlignment(javax.swing.SwingConstants.CENTER); 380 ImageIcon blank = createBlankIcon(); 381 ImageIcon active = ImageProvider.get("dialogs/layerlist", "active"); 382 setIcon(blank); 383 setSelectedIcon(active); 384 setRolloverIcon(blank); 385 setRolloverSelectedIcon(active); 386 setPressedIcon(ImageProvider.get("dialogs/layerlist", "active-pressed")); 387 } 388 } 389 390 private static class LayerVisibleCheckBox extends JCheckBox { 391 private final ImageIcon iconEye; 392 private final ImageIcon iconEyeTranslucent; 393 private boolean isTranslucent; 394 395 /** 396 * Constructs a new {@code LayerVisibleCheckBox}. 397 */ 398 LayerVisibleCheckBox() { 399 setHorizontalAlignment(javax.swing.SwingConstants.RIGHT); 400 iconEye = ImageProvider.get("dialogs/layerlist", "eye"); 401 iconEyeTranslucent = ImageProvider.get("dialogs/layerlist", "eye-translucent"); 402 setIcon(ImageProvider.get("dialogs/layerlist", "eye-off")); 403 setPressedIcon(ImageProvider.get("dialogs/layerlist", "eye-pressed")); 404 setSelectedIcon(iconEye); 405 isTranslucent = false; 406 } 407 408 public void setTranslucent(boolean isTranslucent) { 409 if (this.isTranslucent == isTranslucent) return; 410 if (isTranslucent) { 411 setSelectedIcon(iconEyeTranslucent); 412 } else { 413 setSelectedIcon(iconEye); 414 } 415 this.isTranslucent = isTranslucent; 416 } 417 418 public void updateStatus(Layer layer) { 419 boolean visible = layer.isVisible(); 420 setSelected(visible); 421 List<Layer> layers = MainApplication.getLayerManager().getLayers(); 422 int num = layers.size() - layers.indexOf(layer); 423 setText(String.format("%s[%d]", num < 10 ? " " : "", num)); 424 setTranslucent(layer.getOpacity() < 1.0); 425 setToolTipText(visible ? 426 tr("layer is currently visible (click to hide layer)") : 427 tr("layer is currently hidden (click to show layer)")); 428 } 429 } 430 431 private static class NativeScaleLayerCheckBox extends JCheckBox { 432 NativeScaleLayerCheckBox() { 433 setHorizontalAlignment(javax.swing.SwingConstants.CENTER); 434 ImageIcon blank = createBlankIcon(); 435 ImageIcon active = ImageProvider.get("dialogs/layerlist", "scale"); 436 setIcon(blank); 437 setSelectedIcon(active); 438 } 439 } 440 441 private static class OffsetLayerCheckBox extends JCheckBox { 442 OffsetLayerCheckBox() { 443 setHorizontalAlignment(javax.swing.SwingConstants.CENTER); 444 ImageIcon blank = createBlankIcon(); 445 ImageIcon withOffset = ImageProvider.get("dialogs/layerlist", "offset"); 446 setIcon(blank); 447 setSelectedIcon(withOffset); 448 } 449 } 450 451 private static class ActiveLayerCellRenderer implements TableCellRenderer { 452 private final JCheckBox cb; 453 454 /** 455 * Constructs a new {@code ActiveLayerCellRenderer}. 456 */ 457 ActiveLayerCellRenderer() { 458 cb = new ActiveLayerCheckBox(); 459 } 460 461 @Override 462 public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { 463 boolean active = value != null && (Boolean) value; 464 cb.setSelected(active); 465 cb.setToolTipText(active ? tr("this layer is the active layer") : tr("this layer is not currently active (click to activate)")); 466 return cb; 467 } 468 } 469 470 private static class LayerVisibleCellRenderer implements TableCellRenderer { 471 private final LayerVisibleCheckBox cb; 472 473 /** 474 * Constructs a new {@code LayerVisibleCellRenderer}. 475 */ 476 LayerVisibleCellRenderer() { 477 this.cb = new LayerVisibleCheckBox(); 478 } 479 480 @Override 481 public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { 482 if (value != null) { 483 cb.updateStatus((Layer) value); 484 } 485 return cb; 486 } 487 } 488 489 private static class LayerVisibleCellEditor extends DefaultCellEditor { 490 private final LayerVisibleCheckBox cb; 491 492 LayerVisibleCellEditor(LayerVisibleCheckBox cb) { 493 super(cb); 494 this.cb = cb; 495 } 496 497 @Override 498 public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) { 499 cb.updateStatus((Layer) value); 500 return cb; 501 } 502 } 503 504 private static class NativeScaleLayerCellRenderer implements TableCellRenderer { 505 private final JCheckBox cb; 506 507 /** 508 * Constructs a new {@code ActiveLayerCellRenderer}. 509 */ 510 NativeScaleLayerCellRenderer() { 511 cb = new NativeScaleLayerCheckBox(); 512 } 513 514 @Override 515 public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { 516 Layer layer = (Layer) value; 517 if (layer instanceof NativeScaleLayer) { 518 boolean active = ((NativeScaleLayer) layer) == MainApplication.getMap().mapView.getNativeScaleLayer(); 519 cb.setSelected(active); 520 if (MainApplication.getMap().mapView.getNativeScaleLayer() != null) { 521 cb.setToolTipText(active 522 ? tr("scale follows native resolution of this layer") 523 : tr("scale follows native resolution of another layer (click to set this layer)")); 524 } else { 525 cb.setToolTipText(tr("scale does not follow native resolution of any layer (click to set this layer)")); 526 } 527 } else { 528 cb.setSelected(false); 529 cb.setToolTipText(tr("this layer has no native resolution")); 530 } 531 return cb; 532 } 533 } 534 535 private static class OffsetLayerCellRenderer implements TableCellRenderer { 536 private final JCheckBox cb; 537 538 /** 539 * Constructs a new {@code OffsetLayerCellRenderer}. 540 */ 541 OffsetLayerCellRenderer() { 542 cb = new OffsetLayerCheckBox(); 543 cb.setEnabled(false); 544 } 545 546 @Override 547 public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { 548 Layer layer = (Layer) value; 549 if (layer instanceof AbstractTileSourceLayer<?>) { 550 if (EastNorth.ZERO.equals(((AbstractTileSourceLayer<?>) layer).getDisplaySettings().getDisplacement())) { 551 cb.setSelected(false); 552 cb.setEnabled(false); // TODO: allow reselecting checkbox and thereby setting the old offset again 553 cb.setToolTipText(tr("layer is without a user-defined offset")); 554 } else { 555 cb.setSelected(true); 556 cb.setEnabled(true); 557 cb.setToolTipText(tr("layer has a user-defined offset (click to remove offset)")); 558 } 559 560 } else { 561 cb.setSelected(false); 562 cb.setEnabled(false); 563 cb.setToolTipText(tr("this layer can not have an offset")); 564 } 565 return cb; 566 } 567 } 568 569 private class LayerNameCellRenderer extends DefaultTableCellRenderer { 570 571 protected boolean isActiveLayer(Layer layer) { 572 return getLayerManager().getActiveLayer() == layer; 573 } 574 575 @Override 576 public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { 577 if (value == null) 578 return this; 579 Layer layer = (Layer) value; 580 JLabel label = (JLabel) super.getTableCellRendererComponent(table, 581 layer.getName(), isSelected, hasFocus, row, column); 582 if (isActiveLayer(layer)) { 583 label.setFont(label.getFont().deriveFont(Font.BOLD)); 584 } 585 if (Config.getPref().getBoolean("dialog.layer.colorname", true)) { 586 AbstractProperty<Color> prop = layer.getColorProperty(); 587 Color c = prop == null ? null : prop.get(); 588 if (c == null || model.getLayers().stream() 589 .map(Layer::getColorProperty) 590 .filter(Objects::nonNull) 591 .map(AbstractProperty::get) 592 .noneMatch(oc -> oc != null && !oc.equals(c))) { 593 /* not more than one color, don't use coloring */ 594 label.setForeground(UIManager.getColor(isSelected ? "Table.selectionForeground" : "Table.foreground")); 595 } else { 596 label.setForeground(c); 597 } 598 } 599 label.setIcon(layer.getIcon()); 600 label.setToolTipText(layer.getToolTipText()); 601 return label; 602 } 603 } 604 605 private static class LayerNameCellEditor extends DefaultCellEditor { 606 LayerNameCellEditor(DisableShortcutsOnFocusGainedTextField tf) { 607 super(tf); 608 } 609 610 @Override 611 public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) { 612 JosmTextField tf = (JosmTextField) super.getTableCellEditorComponent(table, value, isSelected, row, column); 613 tf.setText(value == null ? "" : ((Layer) value).getName()); 614 return tf; 615 } 616 } 617 618 class PopupMenuHandler extends PopupMenuLauncher { 619 @Override 620 public void showMenu(MouseEvent evt) { 621 menu = new LayerListPopup(getModel().getSelectedLayers()); 622 super.showMenu(evt); 623 } 624 } 625 626 /** 627 * Observer interface to be implemented by views using {@link LayerListModel}. 628 */ 629 public interface LayerListModelListener { 630 631 /** 632 * Fired when a layer is made visible. 633 * @param index the layer index 634 * @param layer the layer 635 */ 636 void makeVisible(int index, Layer layer); 637 638 639 /** 640 * Fired when something has changed in the layer list model. 641 */ 642 void refresh(); 643 } 644 645 /** 646 * The layer list model. The model manages a list of layers and provides methods for 647 * moving layers up and down, for toggling their visibility, and for activating a layer. 648 * 649 * The model is a {@link TableModel} and it provides a {@link ListSelectionModel}. It expects 650 * to be configured with a {@link DefaultListSelectionModel}. The selection model is used 651 * to update the selection state of views depending on messages sent to the model. 652 * 653 * The model manages a list of {@link LayerListModelListener} which are mainly notified if 654 * the model requires views to make a specific list entry visible. 655 * 656 * It also listens to {@link PropertyChangeEvent}s of every {@link Layer} it manages, in particular to 657 * the properties {@link Layer#VISIBLE_PROP} and {@link Layer#NAME_PROP}. 658 */ 659 public static final class LayerListModel extends AbstractTableModel 660 implements LayerChangeListener, ActiveLayerChangeListener, PropertyChangeListener, ReorderableTableModel<Layer> { 661 /** manages list selection state*/ 662 private final DefaultListSelectionModel selectionModel; 663 private final CopyOnWriteArrayList<LayerListModelListener> listeners; 664 private LayerList layerList; 665 private final MainLayerManager layerManager; 666 667 /** 668 * constructor 669 * @param layerManager The layer manager to use for the list. 670 * @param selectionModel the list selection model 671 */ 672 LayerListModel(MainLayerManager layerManager, DefaultListSelectionModel selectionModel) { 673 this.layerManager = layerManager; 674 this.selectionModel = selectionModel; 675 listeners = new CopyOnWriteArrayList<>(); 676 } 677 678 void setLayerList(LayerList layerList) { 679 this.layerList = layerList; 680 } 681 682 /** 683 * The layer manager this model is for. 684 * @return The layer manager. 685 */ 686 public MainLayerManager getLayerManager() { 687 return layerManager; 688 } 689 690 /** 691 * Adds a listener to this model 692 * 693 * @param listener the listener 694 */ 695 public void addLayerListModelListener(LayerListModelListener listener) { 696 if (listener != null) { 697 listeners.addIfAbsent(listener); 698 } 699 } 700 701 /** 702 * removes a listener from this model 703 * @param listener the listener 704 */ 705 public void removeLayerListModelListener(LayerListModelListener listener) { 706 listeners.remove(listener); 707 } 708 709 /** 710 * Fires a make visible event to listeners 711 * 712 * @param index the index of the row to make visible 713 * @param layer the layer at this index 714 * @see LayerListModelListener#makeVisible(int, Layer) 715 */ 716 private void fireMakeVisible(int index, Layer layer) { 717 for (LayerListModelListener listener : listeners) { 718 listener.makeVisible(index, layer); 719 } 720 } 721 722 /** 723 * Fires a refresh event to listeners of this model 724 * 725 * @see LayerListModelListener#refresh() 726 */ 727 private void fireRefresh() { 728 for (LayerListModelListener listener : listeners) { 729 listener.refresh(); 730 } 731 } 732 733 /** 734 * Populates the model with the current layers managed by {@link MapView}. 735 */ 736 public void populate() { 737 for (Layer layer: getLayers()) { 738 // make sure the model is registered exactly once 739 layer.removePropertyChangeListener(this); 740 layer.addPropertyChangeListener(this); 741 } 742 fireTableDataChanged(); 743 } 744 745 /** 746 * Marks <code>layer</code> as selected layer. Ignored, if layer is null. 747 * 748 * @param layer the layer. 749 */ 750 public void setSelectedLayer(Layer layer) { 751 if (layer == null) 752 return; 753 int idx = getLayers().indexOf(layer); 754 if (idx >= 0) { 755 selectionModel.setSelectionInterval(idx, idx); 756 } 757 ensureSelectedIsVisible(); 758 } 759 760 /** 761 * Replies the list of currently selected layers. Never null, but may be empty. 762 * 763 * @return the list of currently selected layers. Never null, but may be empty. 764 */ 765 public List<Layer> getSelectedLayers() { 766 List<Layer> selected = new ArrayList<>(); 767 List<Layer> layers = getLayers(); 768 for (int i = 0; i < layers.size(); i++) { 769 if (selectionModel.isSelectedIndex(i)) { 770 selected.add(layers.get(i)); 771 } 772 } 773 return selected; 774 } 775 776 /** 777 * Replies a the list of indices of the selected rows. Never null, but may be empty. 778 * 779 * @return the list of indices of the selected rows. Never null, but may be empty. 780 */ 781 public List<Integer> getSelectedRows() { 782 return ArrayUtils.toList(TableHelper.getSelectedIndices(selectionModel)); 783 } 784 785 /** 786 * Invoked if a layer managed by {@link MapView} is removed 787 * 788 * @param layer the layer which is removed 789 */ 790 private void onRemoveLayer(Layer layer) { 791 if (layer == null) 792 return; 793 layer.removePropertyChangeListener(this); 794 final int size = getRowCount(); 795 final int[] rows = TableHelper.getSelectedIndices(selectionModel); 796 797 if (rows.length == 0 && size > 0) { 798 selectionModel.setSelectionInterval(size-1, size-1); 799 } 800 fireTableDataChanged(); 801 fireRefresh(); 802 ensureActiveSelected(); 803 } 804 805 /** 806 * Invoked when a layer managed by {@link MapView} is added 807 * 808 * @param layer the layer 809 */ 810 private void onAddLayer(Layer layer) { 811 if (layer == null) 812 return; 813 layer.addPropertyChangeListener(this); 814 fireTableDataChanged(); 815 int idx = getLayers().indexOf(layer); 816 Icon icon = layer.getIcon(); 817 if (layerList != null && icon != null) { 818 layerList.setRowHeight(idx, Math.max(16, icon.getIconHeight())); 819 } 820 selectionModel.setSelectionInterval(idx, idx); 821 ensureSelectedIsVisible(); 822 if (layer instanceof AbstractTileSourceLayer<?>) { 823 ((AbstractTileSourceLayer<?>) layer).getDisplaySettings().addSettingsChangeListener(LayerListDialog.getInstance()); 824 } 825 } 826 827 /** 828 * Replies the first layer. Null if no layers are present 829 * 830 * @return the first layer. Null if no layers are present 831 */ 832 public Layer getFirstLayer() { 833 if (getRowCount() == 0) 834 return null; 835 return getLayers().get(0); 836 } 837 838 /** 839 * Replies the layer at position <code>index</code> 840 * 841 * @param index the index 842 * @return the layer at position <code>index</code>. Null, 843 * if index is out of range. 844 */ 845 public Layer getLayer(int index) { 846 if (index < 0 || index >= getRowCount()) 847 return null; 848 return getLayers().get(index); 849 } 850 851 @Override 852 public DefaultListSelectionModel getSelectionModel() { 853 return selectionModel; 854 } 855 856 @Override 857 public Layer getValue(int index) { 858 return getLayer(index); 859 } 860 861 @Override 862 public Layer setValue(int index, Layer value) { 863 throw new UnsupportedOperationException(); 864 } 865 866 @Override 867 public boolean doMove(int delta, int... selectedRows) { 868 if (delta != 0) { 869 List<Layer> layers = getLayers(); 870 MapView mapView = MainApplication.getMap().mapView; 871 if (delta < 0) { 872 for (int row : selectedRows) { 873 mapView.moveLayer(layers.get(row), row + delta); 874 } 875 } else { 876 for (int i = selectedRows.length - 1; i >= 0; i--) { 877 mapView.moveLayer(layers.get(selectedRows[i]), selectedRows[i] + delta); 878 } 879 } 880 fireTableDataChanged(); 881 } 882 return delta != 0; 883 } 884 885 @Override 886 public boolean move(int delta, int... selectedRows) { 887 if (!ReorderableTableModel.super.move(delta, selectedRows)) 888 return false; 889 ensureSelectedIsVisible(); 890 return true; 891 } 892 893 /** 894 * Make sure the first of the selected layers is visible in the views of this model. 895 */ 896 private void ensureSelectedIsVisible() { 897 int index = selectionModel.getMinSelectionIndex(); 898 if (index < 0) 899 return; 900 List<Layer> layers = getLayers(); 901 if (index >= layers.size()) 902 return; 903 Layer layer = layers.get(index); 904 fireMakeVisible(index, layer); 905 } 906 907 /** 908 * Replies a list of layers which are possible merge targets for <code>source</code> 909 * 910 * @param source the source layer 911 * @return a list of layers which are possible merge targets 912 * for <code>source</code>. Never null, but can be empty. 913 */ 914 public List<Layer> getPossibleMergeTargets(Layer source) { 915 List<Layer> targets = new ArrayList<>(); 916 if (source == null) { 917 return targets; 918 } 919 for (Layer target : getLayers()) { 920 if (source == target) { 921 continue; 922 } 923 if (target.isMergable(source) && source.isMergable(target)) { 924 targets.add(target); 925 } 926 } 927 return targets; 928 } 929 930 /** 931 * Replies the list of layers currently managed by {@link MapView}. 932 * Never null, but can be empty. 933 * 934 * @return the list of layers currently managed by {@link MapView}. 935 * Never null, but can be empty. 936 */ 937 public List<Layer> getLayers() { 938 return getLayerManager().getLayers(); 939 } 940 941 /** 942 * Ensures that at least one layer is selected in the layer dialog 943 * 944 */ 945 private void ensureActiveSelected() { 946 List<Layer> layers = getLayers(); 947 if (layers.isEmpty()) 948 return; 949 final Layer activeLayer = getActiveLayer(); 950 if (activeLayer != null) { 951 // there's an active layer - select it and make it visible 952 int idx = layers.indexOf(activeLayer); 953 selectionModel.setSelectionInterval(idx, idx); 954 ensureSelectedIsVisible(); 955 } else { 956 // no active layer - select the first one and make it visible 957 selectionModel.setSelectionInterval(0, 0); 958 ensureSelectedIsVisible(); 959 } 960 } 961 962 /** 963 * Replies the active layer. null, if no active layer is available 964 * 965 * @return the active layer. null, if no active layer is available 966 */ 967 private Layer getActiveLayer() { 968 return getLayerManager().getActiveLayer(); 969 } 970 971 /* ------------------------------------------------------------------------------ */ 972 /* Interface TableModel */ 973 /* ------------------------------------------------------------------------------ */ 974 975 @Override 976 public int getRowCount() { 977 List<Layer> layers = getLayers(); 978 return layers == null ? 0 : layers.size(); 979 } 980 981 @Override 982 public int getColumnCount() { 983 return 5; 984 } 985 986 @Override 987 public Object getValueAt(int row, int col) { 988 List<Layer> layers = getLayers(); 989 if (row >= 0 && row < layers.size()) { 990 switch (col) { 991 case 0: return layers.get(row) == getActiveLayer(); 992 case 1: 993 case 2: 994 case 3: 995 case 4: return layers.get(row); 996 default: // Do nothing 997 } 998 } 999 return null; 1000 } 1001 1002 @Override 1003 public boolean isCellEditable(int row, int col) { 1004 return col != 0 || getActiveLayer() != getLayers().get(row); 1005 } 1006 1007 @Override 1008 public void setValueAt(Object value, int row, int col) { 1009 List<Layer> layers = getLayers(); 1010 if (row < layers.size()) { 1011 Layer l = layers.get(row); 1012 switch (col) { 1013 case 0: 1014 getLayerManager().setActiveLayer(l); 1015 l.setVisible(true); 1016 break; 1017 case 1: 1018 MapFrame map = MainApplication.getMap(); 1019 NativeScaleLayer oldLayer = map.mapView.getNativeScaleLayer(); 1020 if (oldLayer == l) { 1021 map.mapView.setNativeScaleLayer(null); 1022 } else if (l instanceof NativeScaleLayer) { 1023 map.mapView.setNativeScaleLayer((NativeScaleLayer) l); 1024 if (oldLayer instanceof Layer) { 1025 int idx = getLayers().indexOf((Layer) oldLayer); 1026 if (idx >= 0) { 1027 fireTableCellUpdated(idx, col); 1028 } 1029 } 1030 } 1031 break; 1032 case 2: 1033 // reset layer offset 1034 if (l instanceof AbstractTileSourceLayer<?>) { 1035 AbstractTileSourceLayer<?> abstractTileSourceLayer = (AbstractTileSourceLayer<?>) l; 1036 OffsetBookmark offsetBookmark = abstractTileSourceLayer.getDisplaySettings().getOffsetBookmark(); 1037 if (offsetBookmark != null) { 1038 abstractTileSourceLayer.getDisplaySettings().setOffsetBookmark(null); 1039 MainApplication.getMenu().imageryMenu.refreshOffsetMenu(); 1040 } 1041 } 1042 break; 1043 case 3: 1044 l.setVisible((Boolean) value); 1045 break; 1046 case 4: 1047 l.rename((String) value); 1048 break; 1049 default: 1050 throw new IllegalArgumentException("Wrong column: " + col); 1051 } 1052 fireTableCellUpdated(row, col); 1053 } 1054 } 1055 1056 /* ------------------------------------------------------------------------------ */ 1057 /* Interface ActiveLayerChangeListener */ 1058 /* ------------------------------------------------------------------------------ */ 1059 @Override 1060 public void activeOrEditLayerChanged(ActiveLayerChangeEvent e) { 1061 Layer oldLayer = e.getPreviousActiveLayer(); 1062 if (oldLayer != null) { 1063 int idx = getLayers().indexOf(oldLayer); 1064 if (idx >= 0) { 1065 fireTableRowsUpdated(idx, idx); 1066 } 1067 } 1068 1069 Layer newLayer = getActiveLayer(); 1070 if (newLayer != null) { 1071 int idx = getLayers().indexOf(newLayer); 1072 if (idx >= 0) { 1073 fireTableRowsUpdated(idx, idx); 1074 } 1075 } 1076 ensureActiveSelected(); 1077 } 1078 1079 /* ------------------------------------------------------------------------------ */ 1080 /* Interface LayerChangeListener */ 1081 /* ------------------------------------------------------------------------------ */ 1082 @Override 1083 public void layerAdded(LayerAddEvent e) { 1084 onAddLayer(e.getAddedLayer()); 1085 } 1086 1087 @Override 1088 public void layerRemoving(LayerRemoveEvent e) { 1089 onRemoveLayer(e.getRemovedLayer()); 1090 } 1091 1092 @Override 1093 public void layerOrderChanged(LayerOrderChangeEvent e) { 1094 fireTableDataChanged(); 1095 } 1096 1097 /* ------------------------------------------------------------------------------ */ 1098 /* Interface PropertyChangeListener */ 1099 /* ------------------------------------------------------------------------------ */ 1100 @Override 1101 public void propertyChange(PropertyChangeEvent evt) { 1102 if (evt.getSource() instanceof Layer) { 1103 Layer layer = (Layer) evt.getSource(); 1104 final int idx = getLayers().indexOf(layer); 1105 if (idx < 0) 1106 return; 1107 fireRefresh(); 1108 } 1109 } 1110 } 1111 1112 /** 1113 * This component displays a list of layers and provides the methods needed by {@link LayerListModel}. 1114 */ 1115 static class LayerList extends ScrollableTable { 1116 1117 LayerList(LayerListModel dataModel) { 1118 super(dataModel); 1119 dataModel.setLayerList(this); 1120 if (!GraphicsEnvironment.isHeadless()) { 1121 setDragEnabled(true); 1122 } 1123 setDropMode(DropMode.INSERT_ROWS); 1124 setTransferHandler(new LayerListTransferHandler()); 1125 } 1126 1127 @Override 1128 public LayerListModel getModel() { 1129 return (LayerListModel) super.getModel(); 1130 } 1131 } 1132 1133 /** 1134 * Creates a {@link ShowHideLayerAction} in the context of this {@link LayerListDialog}. 1135 * 1136 * @return the action 1137 */ 1138 public ShowHideLayerAction createShowHideLayerAction() { 1139 return new ShowHideLayerAction(model); 1140 } 1141 1142 /** 1143 * Creates a {@link DeleteLayerAction} in the context of this {@link LayerListDialog}. 1144 * 1145 * @return the action 1146 */ 1147 public DeleteLayerAction createDeleteLayerAction() { 1148 return new DeleteLayerAction(model); 1149 } 1150 1151 /** 1152 * Creates a {@link ActivateLayerAction} for <code>layer</code> in the context of this {@link LayerListDialog}. 1153 * 1154 * @param layer the layer 1155 * @return the action 1156 */ 1157 public ActivateLayerAction createActivateLayerAction(Layer layer) { 1158 return new ActivateLayerAction(layer, model); 1159 } 1160 1161 /** 1162 * Creates a {@link MergeLayerAction} for <code>layer</code> in the context of this {@link LayerListDialog}. 1163 * 1164 * @param layer the layer 1165 * @return the action 1166 */ 1167 public MergeAction createMergeLayerAction(Layer layer) { 1168 return new MergeAction(layer, model); 1169 } 1170 1171 /** 1172 * Creates a {@link DuplicateAction} for <code>layer</code> in the context of this {@link LayerListDialog}. 1173 * 1174 * @param layer the layer 1175 * @return the action 1176 */ 1177 public DuplicateAction createDuplicateLayerAction(Layer layer) { 1178 return new DuplicateAction(layer, model); 1179 } 1180 1181 /** 1182 * Returns the layer at given index, or {@code null}. 1183 * @param index the index 1184 * @return the layer at given index, or {@code null} if index out of range 1185 */ 1186 public static Layer getLayerForIndex(int index) { 1187 List<Layer> layers = MainApplication.getLayerManager().getLayers(); 1188 1189 if (index < layers.size() && index >= 0) 1190 return layers.get(index); 1191 else 1192 return null; 1193 } 1194 1195 /** 1196 * Returns a list of info on all layers of a given class. 1197 * @param layerClass The layer class. This is not {@code Class<? extends Layer>} on purpose, 1198 * to allow asking for layers implementing some interface 1199 * @return list of info on all layers assignable from {@code layerClass} 1200 */ 1201 public static List<MultikeyInfo> getLayerInfoByClass(Class<?> layerClass) { 1202 List<MultikeyInfo> result = new ArrayList<>(); 1203 1204 List<Layer> layers = MainApplication.getLayerManager().getLayers(); 1205 1206 int index = 0; 1207 for (Layer l: layers) { 1208 if (layerClass.isAssignableFrom(l.getClass())) { 1209 result.add(new MultikeyInfo(index, l.getName())); 1210 } 1211 index++; 1212 } 1213 1214 return result; 1215 } 1216 1217 /** 1218 * Determines if a layer is valid (contained in global layer list). 1219 * @param l the layer 1220 * @return {@code true} if layer {@code l} is contained in current layer list 1221 */ 1222 public static boolean isLayerValid(Layer l) { 1223 if (l == null) 1224 return false; 1225 1226 return MainApplication.getLayerManager().containsLayer(l); 1227 } 1228 1229 /** 1230 * Returns info about layer. 1231 * @param l the layer 1232 * @return info about layer {@code l} 1233 */ 1234 public static MultikeyInfo getLayerInfo(Layer l) { 1235 if (l == null) 1236 return null; 1237 1238 int index = MainApplication.getLayerManager().getLayers().indexOf(l); 1239 if (index < 0) 1240 return null; 1241 1242 return new MultikeyInfo(index, l.getName()); 1243 } 1244 1245 @Override 1246 public void displaySettingsChanged(DisplaySettingsChangeEvent e) { 1247 if ("displacement".equals(e.getChangedSetting())) { 1248 layerList.repaint(); 1249 } 1250 } 1251}