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