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