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