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.Point; 011import java.awt.Rectangle; 012import java.awt.event.ActionEvent; 013import java.awt.event.InputEvent; 014import java.awt.event.KeyEvent; 015import java.awt.event.MouseEvent; 016import java.beans.PropertyChangeEvent; 017import java.beans.PropertyChangeListener; 018import java.lang.ref.WeakReference; 019import java.util.ArrayList; 020import java.util.Arrays; 021import java.util.Collections; 022import java.util.List; 023import java.util.concurrent.CopyOnWriteArrayList; 024 025import javax.swing.AbstractAction; 026import javax.swing.DefaultCellEditor; 027import javax.swing.DefaultListSelectionModel; 028import javax.swing.ImageIcon; 029import javax.swing.JCheckBox; 030import javax.swing.JComponent; 031import javax.swing.JLabel; 032import javax.swing.JMenuItem; 033import javax.swing.JPopupMenu; 034import javax.swing.JSlider; 035import javax.swing.JTable; 036import javax.swing.JViewport; 037import javax.swing.KeyStroke; 038import javax.swing.ListSelectionModel; 039import javax.swing.UIManager; 040import javax.swing.event.ChangeEvent; 041import javax.swing.event.ChangeListener; 042import javax.swing.event.ListDataEvent; 043import javax.swing.event.ListSelectionEvent; 044import javax.swing.event.ListSelectionListener; 045import javax.swing.event.TableModelEvent; 046import javax.swing.event.TableModelListener; 047import javax.swing.table.AbstractTableModel; 048import javax.swing.table.DefaultTableCellRenderer; 049import javax.swing.table.TableCellRenderer; 050import javax.swing.table.TableModel; 051 052import org.openstreetmap.josm.Main; 053import org.openstreetmap.josm.actions.MergeLayerAction; 054import org.openstreetmap.josm.gui.MapFrame; 055import org.openstreetmap.josm.gui.MapView; 056import org.openstreetmap.josm.gui.SideButton; 057import org.openstreetmap.josm.gui.help.HelpUtil; 058import org.openstreetmap.josm.gui.layer.ImageryLayer; 059import org.openstreetmap.josm.gui.layer.JumpToMarkerActions; 060import org.openstreetmap.josm.gui.layer.Layer; 061import org.openstreetmap.josm.gui.layer.Layer.LayerAction; 062import org.openstreetmap.josm.gui.layer.NativeScaleLayer; 063import org.openstreetmap.josm.gui.layer.OsmDataLayer; 064import org.openstreetmap.josm.gui.util.GuiHelper; 065import org.openstreetmap.josm.gui.widgets.DisableShortcutsOnFocusGainedTextField; 066import org.openstreetmap.josm.gui.widgets.JosmTextField; 067import org.openstreetmap.josm.gui.widgets.PopupMenuLauncher; 068import org.openstreetmap.josm.tools.CheckParameterUtil; 069import org.openstreetmap.josm.tools.ImageProvider; 070import org.openstreetmap.josm.tools.InputMapUtils; 071import org.openstreetmap.josm.tools.MultikeyActionsHandler; 072import org.openstreetmap.josm.tools.MultikeyShortcutAction; 073import org.openstreetmap.josm.tools.MultikeyShortcutAction.MultikeyInfo; 074import org.openstreetmap.josm.tools.Shortcut; 075import org.openstreetmap.josm.tools.Utils; 076 077/** 078 * This is a toggle dialog which displays the list of layers. Actions allow to 079 * change the ordering of the layers, to hide/show layers, to activate layers, 080 * and to delete layers. 081 * @since 17 082 */ 083public class LayerListDialog extends ToggleDialog { 084 /** the unique instance of the dialog */ 085 private static volatile LayerListDialog instance; 086 087 /** 088 * Creates the instance of the dialog. It's connected to the map frame <code>mapFrame</code> 089 * 090 * @param mapFrame the map frame 091 */ 092 public static void createInstance(MapFrame mapFrame) { 093 if (instance != null) 094 throw new IllegalStateException("Dialog was already created"); 095 instance = new LayerListDialog(mapFrame); 096 } 097 098 /** 099 * Replies the instance of the dialog 100 * 101 * @return the instance of the dialog 102 * @throws IllegalStateException if the dialog is not created yet 103 * @see #createInstance(MapFrame) 104 */ 105 public static LayerListDialog getInstance() { 106 if (instance == null) 107 throw new IllegalStateException("Dialog not created yet. Invoke createInstance() first"); 108 return instance; 109 } 110 111 /** the model for the layer list */ 112 private final LayerListModel model; 113 114 /** the list of layers (technically its a JTable, but appears like a list) */ 115 private final LayerList layerList; 116 117 private final ActivateLayerAction activateLayerAction; 118 private final ShowHideLayerAction showHideLayerAction; 119 120 //TODO This duplicates ShowHide actions functionality 121 /** stores which layer index to toggle and executes the ShowHide action if the layer is present */ 122 private final class ToggleLayerIndexVisibility extends AbstractAction { 123 private final int layerIndex; 124 125 ToggleLayerIndexVisibility(int layerIndex) { 126 this.layerIndex = layerIndex; 127 } 128 129 @Override 130 public void actionPerformed(ActionEvent e) { 131 final Layer l = model.getLayer(model.getRowCount() - layerIndex - 1); 132 if (l != null) { 133 l.toggleVisible(); 134 } 135 } 136 } 137 138 private final transient Shortcut[] visibilityToggleShortcuts = new Shortcut[10]; 139 private final ToggleLayerIndexVisibility[] visibilityToggleActions = new ToggleLayerIndexVisibility[10]; 140 141 /** 142 * registers (shortcut to toggle right hand side toggle dialogs)+(number keys) shortcuts 143 * to toggle the visibility of the first ten layers. 144 */ 145 private void createVisibilityToggleShortcuts() { 146 for (int i = 0; i < 10; i++) { 147 final int i1 = i + 1; 148 /* POSSIBLE SHORTCUTS: 1,2,3,4,5,6,7,8,9,0=10 */ 149 visibilityToggleShortcuts[i] = Shortcut.registerShortcut("subwindow:layers:toggleLayer" + i1, 150 tr("Toggle visibility of layer: {0}", i1), KeyEvent.VK_0 + (i1 % 10), Shortcut.ALT); 151 visibilityToggleActions[i] = new ToggleLayerIndexVisibility(i); 152 Main.registerActionShortcut(visibilityToggleActions[i], visibilityToggleShortcuts[i]); 153 } 154 } 155 156 /** 157 * Creates a layer list and attach it to the given mapView. 158 * @param mapFrame map frame 159 */ 160 protected LayerListDialog(MapFrame mapFrame) { 161 super(tr("Layers"), "layerlist", tr("Open a list of all loaded layers."), 162 Shortcut.registerShortcut("subwindow:layers", tr("Toggle: {0}", tr("Layers")), KeyEvent.VK_L, 163 Shortcut.ALT_SHIFT), 100, true); 164 165 // create the models 166 // 167 DefaultListSelectionModel selectionModel = new DefaultListSelectionModel(); 168 selectionModel.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); 169 model = new LayerListModel(selectionModel); 170 171 // create the list control 172 // 173 layerList = new LayerList(model); 174 layerList.setSelectionModel(selectionModel); 175 layerList.addMouseListener(new PopupMenuHandler()); 176 layerList.setBackground(UIManager.getColor("Button.background")); 177 layerList.putClientProperty("terminateEditOnFocusLost", Boolean.TRUE); 178 layerList.putClientProperty("JTable.autoStartsEdit", Boolean.FALSE); 179 layerList.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); 180 layerList.setTableHeader(null); 181 layerList.setShowGrid(false); 182 layerList.setIntercellSpacing(new Dimension(0, 0)); 183 layerList.getColumnModel().getColumn(0).setCellRenderer(new ActiveLayerCellRenderer()); 184 layerList.getColumnModel().getColumn(0).setCellEditor(new DefaultCellEditor(new ActiveLayerCheckBox())); 185 layerList.getColumnModel().getColumn(0).setMaxWidth(12); 186 layerList.getColumnModel().getColumn(0).setPreferredWidth(12); 187 layerList.getColumnModel().getColumn(0).setResizable(false); 188 189 layerList.getColumnModel().getColumn(1).setCellRenderer(new NativeScaleLayerCellRenderer()); 190 layerList.getColumnModel().getColumn(1).setCellEditor(new DefaultCellEditor(new NativeScaleLayerCheckBox())); 191 layerList.getColumnModel().getColumn(1).setMaxWidth(12); 192 layerList.getColumnModel().getColumn(1).setPreferredWidth(12); 193 layerList.getColumnModel().getColumn(1).setResizable(false); 194 195 layerList.getColumnModel().getColumn(2).setCellRenderer(new LayerVisibleCellRenderer()); 196 layerList.getColumnModel().getColumn(2).setCellEditor(new LayerVisibleCellEditor(new LayerVisibleCheckBox())); 197 layerList.getColumnModel().getColumn(2).setMaxWidth(16); 198 layerList.getColumnModel().getColumn(2).setPreferredWidth(16); 199 layerList.getColumnModel().getColumn(2).setResizable(false); 200 201 layerList.getColumnModel().getColumn(3).setCellRenderer(new LayerNameCellRenderer()); 202 layerList.getColumnModel().getColumn(3).setCellEditor(new LayerNameCellEditor(new DisableShortcutsOnFocusGainedTextField())); 203 // Disable some default JTable shortcuts to use JOSM ones (see #5678, #10458) 204 for (KeyStroke ks : new KeyStroke[] { 205 KeyStroke.getKeyStroke(KeyEvent.VK_C, GuiHelper.getMenuShortcutKeyMaskEx()), 206 KeyStroke.getKeyStroke(KeyEvent.VK_V, GuiHelper.getMenuShortcutKeyMaskEx()), 207 KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, InputEvent.SHIFT_DOWN_MASK), 208 KeyStroke.getKeyStroke(KeyEvent.VK_UP, InputEvent.SHIFT_DOWN_MASK), 209 KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, InputEvent.SHIFT_DOWN_MASK), 210 KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, InputEvent.SHIFT_DOWN_MASK), 211 KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, InputEvent.CTRL_DOWN_MASK), 212 KeyStroke.getKeyStroke(KeyEvent.VK_UP, InputEvent.CTRL_DOWN_MASK), 213 KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, InputEvent.CTRL_DOWN_MASK), 214 KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, InputEvent.CTRL_DOWN_MASK), 215 KeyStroke.getKeyStroke(KeyEvent.VK_PAGE_UP, 0), 216 KeyStroke.getKeyStroke(KeyEvent.VK_PAGE_DOWN, 0), 217 KeyStroke.getKeyStroke(KeyEvent.VK_TAB, 0), 218 KeyStroke.getKeyStroke(KeyEvent.VK_F8, 0), 219 }) { 220 layerList.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(ks, new Object()); 221 } 222 223 // init the model 224 // 225 final MapView mapView = mapFrame.mapView; 226 model.populate(); 227 model.setSelectedLayer(mapView.getActiveLayer()); 228 model.addLayerListModelListener( 229 new LayerListModelListener() { 230 @Override 231 public void makeVisible(int row, Layer layer) { 232 layerList.scrollToVisible(row, 0); 233 layerList.repaint(); 234 } 235 236 @Override 237 public void refresh() { 238 layerList.repaint(); 239 } 240 } 241 ); 242 243 // -- move up action 244 MoveUpAction moveUpAction = new MoveUpAction(); 245 adaptTo(moveUpAction, model); 246 adaptTo(moveUpAction, selectionModel); 247 248 // -- move down action 249 MoveDownAction moveDownAction = new MoveDownAction(); 250 adaptTo(moveDownAction, model); 251 adaptTo(moveDownAction, selectionModel); 252 253 // -- activate action 254 activateLayerAction = new ActivateLayerAction(); 255 activateLayerAction.updateEnabledState(); 256 MultikeyActionsHandler.getInstance().addAction(activateLayerAction); 257 adaptTo(activateLayerAction, selectionModel); 258 259 JumpToMarkerActions.initialize(); 260 261 // -- show hide action 262 showHideLayerAction = new ShowHideLayerAction(); 263 MultikeyActionsHandler.getInstance().addAction(showHideLayerAction); 264 adaptTo(showHideLayerAction, selectionModel); 265 266 // -- layer opacity action 267 LayerOpacityAction layerOpacityAction = new LayerOpacityAction(model); 268 adaptTo(layerOpacityAction, selectionModel); 269 SideButton opacityButton = new SideButton(layerOpacityAction, false); 270 layerOpacityAction.setCorrespondingSideButton(opacityButton); 271 272 // -- layer gamma action 273 LayerGammaAction layerGammaAction = new LayerGammaAction(model); 274 adaptTo(layerGammaAction, selectionModel); 275 SideButton gammaButton = new SideButton(layerGammaAction, false); 276 layerGammaAction.setCorrespondingSideButton(gammaButton); 277 278 // -- delete layer action 279 DeleteLayerAction deleteLayerAction = new DeleteLayerAction(); 280 layerList.getActionMap().put("deleteLayer", deleteLayerAction); 281 adaptTo(deleteLayerAction, selectionModel); 282 getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put( 283 KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0), "delete" 284 ); 285 getActionMap().put("delete", deleteLayerAction); 286 287 // Activate layer on Enter key press 288 InputMapUtils.addEnterAction(layerList, new AbstractAction() { 289 @Override 290 public void actionPerformed(ActionEvent e) { 291 activateLayerAction.actionPerformed(null); 292 layerList.requestFocus(); 293 } 294 }); 295 296 // Show/Activate layer on Enter key press 297 InputMapUtils.addSpacebarAction(layerList, showHideLayerAction); 298 299 createLayout(layerList, true, Arrays.asList( 300 new SideButton(moveUpAction, false), 301 new SideButton(moveDownAction, false), 302 new SideButton(activateLayerAction, false), 303 new SideButton(showHideLayerAction, false), 304 opacityButton, 305 gammaButton, 306 new SideButton(deleteLayerAction, false) 307 )); 308 309 createVisibilityToggleShortcuts(); 310 } 311 312 @Override 313 public void showNotify() { 314 MapView.addLayerChangeListener(activateLayerAction); 315 MapView.addLayerChangeListener(model); 316 model.populate(); 317 } 318 319 @Override 320 public void hideNotify() { 321 MapView.removeLayerChangeListener(model); 322 MapView.removeLayerChangeListener(activateLayerAction); 323 } 324 325 /** 326 * Returns the layer list model. 327 * @return the layer list model 328 */ 329 public LayerListModel getModel() { 330 return model; 331 } 332 333 protected interface IEnabledStateUpdating { 334 void updateEnabledState(); 335 } 336 337 /** 338 * Wires <code>listener</code> to <code>listSelectionModel</code> in such a way, that 339 * <code>listener</code> receives a {@link IEnabledStateUpdating#updateEnabledState()} 340 * on every {@link ListSelectionEvent}. 341 * 342 * @param listener the listener 343 * @param listSelectionModel the source emitting {@link ListSelectionEvent}s 344 */ 345 protected void adaptTo(final IEnabledStateUpdating listener, ListSelectionModel listSelectionModel) { 346 listSelectionModel.addListSelectionListener( 347 new ListSelectionListener() { 348 @Override 349 public void valueChanged(ListSelectionEvent e) { 350 listener.updateEnabledState(); 351 } 352 } 353 ); 354 } 355 356 /** 357 * Wires <code>listener</code> to <code>listModel</code> in such a way, that 358 * <code>listener</code> receives a {@link IEnabledStateUpdating#updateEnabledState()} 359 * on every {@link ListDataEvent}. 360 * 361 * @param listener the listener 362 * @param listModel the source emitting {@link ListDataEvent}s 363 */ 364 protected void adaptTo(final IEnabledStateUpdating listener, LayerListModel listModel) { 365 listModel.addTableModelListener( 366 new TableModelListener() { 367 368 @Override 369 public void tableChanged(TableModelEvent e) { 370 listener.updateEnabledState(); 371 } 372 } 373 ); 374 } 375 376 @Override 377 public void destroy() { 378 for (int i = 0; i < 10; i++) { 379 Main.unregisterActionShortcut(visibilityToggleActions[i], visibilityToggleShortcuts[i]); 380 } 381 MultikeyActionsHandler.getInstance().removeAction(activateLayerAction); 382 MultikeyActionsHandler.getInstance().removeAction(showHideLayerAction); 383 JumpToMarkerActions.unregisterActions(); 384 super.destroy(); 385 instance = null; 386 } 387 388 /** 389 * The action to delete the currently selected layer 390 */ 391 public final class DeleteLayerAction extends AbstractAction implements IEnabledStateUpdating, LayerAction { 392 393 /** 394 * Creates a {@link DeleteLayerAction} which will delete the currently 395 * selected layers in the layer dialog. 396 */ 397 public DeleteLayerAction() { 398 putValue(SMALL_ICON, ImageProvider.get("dialogs", "delete")); 399 putValue(SHORT_DESCRIPTION, tr("Delete the selected layers.")); 400 putValue(NAME, tr("Delete")); 401 putValue("help", HelpUtil.ht("/Dialog/LayerList#DeleteLayer")); 402 updateEnabledState(); 403 } 404 405 @Override 406 public void actionPerformed(ActionEvent e) { 407 List<Layer> selectedLayers = getModel().getSelectedLayers(); 408 if (selectedLayers.isEmpty()) 409 return; 410 if (!Main.saveUnsavedModifications(selectedLayers, false)) 411 return; 412 for (Layer l: selectedLayers) { 413 Main.main.removeLayer(l); 414 } 415 } 416 417 @Override 418 public void updateEnabledState() { 419 setEnabled(!getModel().getSelectedLayers().isEmpty()); 420 } 421 422 @Override 423 public Component createMenuComponent() { 424 return new JMenuItem(this); 425 } 426 427 @Override 428 public boolean supportLayers(List<Layer> layers) { 429 return true; 430 } 431 432 @Override 433 public boolean equals(Object obj) { 434 return obj instanceof DeleteLayerAction; 435 } 436 437 @Override 438 public int hashCode() { 439 return getClass().hashCode(); 440 } 441 } 442 443 /** 444 * Action which will toggle the visibility of the currently selected layers. 445 */ 446 public final class ShowHideLayerAction extends AbstractAction implements IEnabledStateUpdating, LayerAction, MultikeyShortcutAction { 447 448 private transient WeakReference<Layer> lastLayer; 449 private final transient Shortcut multikeyShortcut; 450 451 /** 452 * Creates a {@link ShowHideLayerAction} which will toggle the visibility of 453 * the currently selected layers 454 */ 455 public ShowHideLayerAction() { 456 putValue(NAME, tr("Show/hide")); 457 putValue(SMALL_ICON, ImageProvider.get("dialogs", "showhide")); 458 putValue(SHORT_DESCRIPTION, tr("Toggle visible state of the selected layer.")); 459 putValue("help", HelpUtil.ht("/Dialog/LayerList#ShowHideLayer")); 460 multikeyShortcut = Shortcut.registerShortcut("core_multikey:showHideLayer", tr("Multikey: {0}", 461 tr("Show/hide layer")), KeyEvent.VK_S, Shortcut.SHIFT); 462 multikeyShortcut.setAccelerator(this); 463 updateEnabledState(); 464 } 465 466 @Override 467 public Shortcut getMultikeyShortcut() { 468 return multikeyShortcut; 469 } 470 471 @Override 472 public void actionPerformed(ActionEvent e) { 473 for (Layer l : model.getSelectedLayers()) { 474 l.toggleVisible(); 475 } 476 } 477 478 @Override 479 public void executeMultikeyAction(int index, boolean repeat) { 480 Layer l = LayerListDialog.getLayerForIndex(index); 481 if (l != null) { 482 l.toggleVisible(); 483 lastLayer = new WeakReference<>(l); 484 } else if (repeat && lastLayer != null) { 485 l = lastLayer.get(); 486 if (LayerListDialog.isLayerValid(l)) { 487 l.toggleVisible(); 488 } 489 } 490 } 491 492 @Override 493 public void updateEnabledState() { 494 setEnabled(!model.getSelectedLayers().isEmpty()); 495 } 496 497 @Override 498 public Component createMenuComponent() { 499 return new JMenuItem(this); 500 } 501 502 @Override 503 public boolean supportLayers(List<Layer> layers) { 504 return true; 505 } 506 507 @Override 508 public boolean equals(Object obj) { 509 return obj instanceof ShowHideLayerAction; 510 } 511 512 @Override 513 public int hashCode() { 514 return getClass().hashCode(); 515 } 516 517 @Override 518 public List<MultikeyInfo> getMultikeyCombinations() { 519 return LayerListDialog.getLayerInfoByClass(Layer.class); 520 } 521 522 @Override 523 public MultikeyInfo getLastMultikeyAction() { 524 if (lastLayer != null) 525 return LayerListDialog.getLayerInfo(lastLayer.get()); 526 return null; 527 } 528 } 529 530 /** 531 * Abstract action which allows to adjust a double value using a slider 532 */ 533 public abstract static class AbstractLayerPropertySliderAction extends AbstractAction implements IEnabledStateUpdating, LayerAction { 534 protected final LayerListModel model; 535 protected final JPopupMenu popup; 536 protected final JSlider slider; 537 private final double factor; 538 private SideButton sideButton; 539 540 protected AbstractLayerPropertySliderAction(LayerListModel model, String name, final double factor) { 541 super(name); 542 this.model = model; 543 this.factor = factor; 544 updateEnabledState(); 545 546 popup = new JPopupMenu(); 547 slider = new JSlider(JSlider.VERTICAL); 548 slider.addChangeListener(new ChangeListener() { 549 @Override 550 public void stateChanged(ChangeEvent e) { 551 setValue(slider.getValue() / factor); 552 } 553 }); 554 popup.add(slider); 555 } 556 557 protected abstract void setValue(double value); 558 559 protected abstract double getValue(); 560 561 /** 562 * Sets the corresponding side button. 563 * @param sideButton the corresponding side button 564 */ 565 final void setCorrespondingSideButton(SideButton sideButton) { 566 this.sideButton = sideButton; 567 } 568 569 @Override 570 public void actionPerformed(ActionEvent e) { 571 slider.setValue((int) (getValue() * factor)); 572 if (e.getSource() == sideButton) { 573 popup.show(sideButton, 0, sideButton.getHeight()); 574 } else { 575 // Action can be trigger either by opacity button or by popup menu (in case toggle buttons are hidden). 576 // In that case, show it in the middle of screen (because opacityButton is not visible) 577 popup.show(Main.parent, Main.parent.getWidth() / 2, (Main.parent.getHeight() - popup.getHeight()) / 2); 578 } 579 } 580 581 @Override 582 public Component createMenuComponent() { 583 return new JMenuItem(this); 584 } 585 } 586 587 /** 588 * Action which allows to change the opacity of one or more layers. 589 */ 590 public static final class LayerOpacityAction extends AbstractLayerPropertySliderAction { 591 private transient Layer layer; 592 593 /** 594 * Creates a {@link LayerOpacityAction} which allows to change the opacity of one or more layers. 595 * 596 * @param model layer list model 597 * @param layer the layer. Must not be null. 598 * @throws IllegalArgumentException if layer is null 599 */ 600 public LayerOpacityAction(LayerListModel model, Layer layer) { 601 this(model); 602 CheckParameterUtil.ensureParameterNotNull(layer, "layer"); 603 this.layer = layer; 604 updateEnabledState(); 605 } 606 607 /** 608 * Creates a {@link ShowHideLayerAction} which will toggle the visibility of the currently selected layers 609 * @param model layer list model 610 */ 611 public LayerOpacityAction(LayerListModel model) { 612 super(model, tr("Opacity"), 100); 613 putValue(SHORT_DESCRIPTION, tr("Adjust opacity of the layer.")); 614 putValue(SMALL_ICON, ImageProvider.get("dialogs/layerlist", "transparency")); 615 } 616 617 @Override 618 protected void setValue(double value) { 619 if (!isEnabled()) return; 620 if (layer != null) { 621 layer.setOpacity(value); 622 } else { 623 for (Layer l : model.getSelectedLayers()) { 624 l.setOpacity(value); 625 } 626 } 627 } 628 629 @Override 630 protected double getValue() { 631 if (layer != null) 632 return layer.getOpacity(); 633 else { 634 double opacity = 0; 635 List<Layer> layers = model.getSelectedLayers(); 636 for (Layer l : layers) { 637 opacity += l.getOpacity(); 638 } 639 return opacity / layers.size(); 640 } 641 } 642 643 @Override 644 public void updateEnabledState() { 645 if (layer == null) { 646 setEnabled(!model.getSelectedLayers().isEmpty()); 647 } else { 648 setEnabled(true); 649 } 650 } 651 652 @Override 653 public boolean supportLayers(List<Layer> layers) { 654 return true; 655 } 656 } 657 658 /** 659 * Action which allows to change the gamma of one imagery layer. 660 */ 661 public static final class LayerGammaAction extends AbstractLayerPropertySliderAction { 662 663 /** 664 * Constructs a new {@code LayerGammaAction}. 665 * @param model layer list model 666 */ 667 public LayerGammaAction(LayerListModel model) { 668 super(model, tr("Gamma"), 50); 669 putValue(SHORT_DESCRIPTION, tr("Adjust gamma value of the layer.")); 670 putValue(SMALL_ICON, ImageProvider.get("dialogs/layerlist", "gamma")); 671 } 672 673 @Override 674 protected void setValue(double value) { 675 for (ImageryLayer imageryLayer : Utils.filteredCollection(model.getSelectedLayers(), ImageryLayer.class)) { 676 imageryLayer.setGamma(value); 677 } 678 } 679 680 @Override 681 protected double getValue() { 682 return Utils.filteredCollection(model.getSelectedLayers(), ImageryLayer.class).iterator().next().getGamma(); 683 } 684 685 @Override 686 public void updateEnabledState() { 687 setEnabled(!Utils.filteredCollection(model.getSelectedLayers(), ImageryLayer.class).isEmpty()); 688 } 689 690 @Override 691 public boolean supportLayers(List<Layer> layers) { 692 return !Utils.filteredCollection(layers, ImageryLayer.class).isEmpty(); 693 } 694 } 695 696 /** 697 * The action to activate the currently selected layer 698 */ 699 700 public final class ActivateLayerAction extends AbstractAction 701 implements IEnabledStateUpdating, MapView.LayerChangeListener, MultikeyShortcutAction { 702 private transient Layer layer; 703 private transient Shortcut multikeyShortcut; 704 705 /** 706 * Constructs a new {@code ActivateLayerAction}. 707 * @param layer the layer 708 */ 709 public ActivateLayerAction(Layer layer) { 710 this(); 711 CheckParameterUtil.ensureParameterNotNull(layer, "layer"); 712 this.layer = layer; 713 putValue(NAME, tr("Activate")); 714 updateEnabledState(); 715 } 716 717 /** 718 * Constructs a new {@code ActivateLayerAction}. 719 */ 720 public ActivateLayerAction() { 721 putValue(NAME, tr("Activate")); 722 putValue(SMALL_ICON, ImageProvider.get("dialogs", "activate")); 723 putValue(SHORT_DESCRIPTION, tr("Activate the selected layer")); 724 multikeyShortcut = Shortcut.registerShortcut("core_multikey:activateLayer", tr("Multikey: {0}", 725 tr("Activate layer")), KeyEvent.VK_A, Shortcut.SHIFT); 726 multikeyShortcut.setAccelerator(this); 727 putValue("help", HelpUtil.ht("/Dialog/LayerList#ActivateLayer")); 728 } 729 730 @Override 731 public Shortcut getMultikeyShortcut() { 732 return multikeyShortcut; 733 } 734 735 @Override 736 public void actionPerformed(ActionEvent e) { 737 Layer toActivate; 738 if (layer != null) { 739 toActivate = layer; 740 } else { 741 toActivate = model.getSelectedLayers().get(0); 742 } 743 execute(toActivate); 744 } 745 746 private void execute(Layer layer) { 747 // model is going to be updated via LayerChangeListener and PropertyChangeEvents 748 Main.map.mapView.setActiveLayer(layer); 749 layer.setVisible(true); 750 } 751 752 protected boolean isActiveLayer(Layer layer) { 753 if (!Main.isDisplayingMapView()) return false; 754 return Main.map.mapView.getActiveLayer() == layer; 755 } 756 757 @Override 758 public void updateEnabledState() { 759 GuiHelper.runInEDTAndWait(new Runnable() { 760 @Override 761 public void run() { 762 if (layer == null) { 763 if (getModel().getSelectedLayers().size() != 1) { 764 setEnabled(false); 765 return; 766 } 767 Layer selectedLayer = getModel().getSelectedLayers().get(0); 768 setEnabled(!isActiveLayer(selectedLayer)); 769 } else { 770 setEnabled(!isActiveLayer(layer)); 771 } 772 } 773 }); 774 } 775 776 @Override 777 public void activeLayerChange(Layer oldLayer, Layer newLayer) { 778 updateEnabledState(); 779 } 780 781 @Override 782 public void layerAdded(Layer newLayer) { 783 updateEnabledState(); 784 } 785 786 @Override 787 public void layerRemoved(Layer oldLayer) { 788 updateEnabledState(); 789 } 790 791 @Override 792 public void executeMultikeyAction(int index, boolean repeat) { 793 Layer l = LayerListDialog.getLayerForIndex(index); 794 if (l != null) { 795 execute(l); 796 } 797 } 798 799 @Override 800 public List<MultikeyInfo> getMultikeyCombinations() { 801 return LayerListDialog.getLayerInfoByClass(Layer.class); 802 } 803 804 @Override 805 public MultikeyInfo getLastMultikeyAction() { 806 return null; // Repeating action doesn't make much sense for activating 807 } 808 } 809 810 /** 811 * The action to merge the currently selected layer into another layer. 812 */ 813 public final class MergeAction extends AbstractAction implements IEnabledStateUpdating, LayerAction, Layer.MultiLayerAction { 814 private transient Layer layer; 815 private transient List<Layer> layers; 816 817 /** 818 * Constructs a new {@code MergeAction}. 819 * @param layer the layer 820 * @throws IllegalArgumentException if {@code layer} is null 821 */ 822 public MergeAction(Layer layer) { 823 this(layer, null); 824 CheckParameterUtil.ensureParameterNotNull(layer, "layer"); 825 } 826 827 /** 828 * Constructs a new {@code MergeAction}. 829 * @param layers the layer list 830 * @throws IllegalArgumentException if {@code layers} is null 831 */ 832 public MergeAction(List<Layer> layers) { 833 this(null, layers); 834 CheckParameterUtil.ensureParameterNotNull(layers, "layers"); 835 } 836 837 /** 838 * Constructs a new {@code MergeAction}. 839 * @param layer the layer (null if layer list if specified) 840 * @param layers the layer list (null if a single layer is specified) 841 */ 842 private MergeAction(Layer layer, List<Layer> layers) { 843 this.layer = layer; 844 this.layers = layers; 845 putValue(NAME, tr("Merge")); 846 putValue(SMALL_ICON, ImageProvider.get("dialogs", "mergedown")); 847 putValue(SHORT_DESCRIPTION, tr("Merge this layer into another layer")); 848 putValue("help", HelpUtil.ht("/Dialog/LayerList#MergeLayer")); 849 updateEnabledState(); 850 } 851 852 @Override 853 public void actionPerformed(ActionEvent e) { 854 if (layer != null) { 855 Main.main.menu.merge.merge(layer); 856 } else if (layers != null) { 857 Main.main.menu.merge.merge(layers); 858 } else { 859 if (getModel().getSelectedLayers().size() == 1) { 860 Layer selectedLayer = getModel().getSelectedLayers().get(0); 861 Main.main.menu.merge.merge(selectedLayer); 862 } else { 863 Main.main.menu.merge.merge(getModel().getSelectedLayers()); 864 } 865 } 866 } 867 868 @Override 869 public void updateEnabledState() { 870 if (layer == null && layers == null) { 871 if (getModel().getSelectedLayers().isEmpty()) { 872 setEnabled(false); 873 } else if (getModel().getSelectedLayers().size() > 1) { 874 setEnabled(supportLayers(getModel().getSelectedLayers())); 875 } else { 876 Layer selectedLayer = getModel().getSelectedLayers().get(0); 877 List<Layer> targets = getModel().getPossibleMergeTargets(selectedLayer); 878 setEnabled(!targets.isEmpty()); 879 } 880 } else if (layer != null) { 881 List<Layer> targets = getModel().getPossibleMergeTargets(layer); 882 setEnabled(!targets.isEmpty()); 883 } else { 884 setEnabled(supportLayers(layers)); 885 } 886 } 887 888 @Override 889 public boolean supportLayers(List<Layer> layers) { 890 if (layers.isEmpty()) { 891 return false; 892 } else { 893 final Layer firstLayer = layers.get(0); 894 final List<Layer> remainingLayers = layers.subList(1, layers.size()); 895 return getModel().getPossibleMergeTargets(firstLayer).containsAll(remainingLayers); 896 } 897 } 898 899 @Override 900 public Component createMenuComponent() { 901 return new JMenuItem(this); 902 } 903 904 @Override 905 public MergeAction getMultiLayerAction(List<Layer> layers) { 906 return new MergeAction(layers); 907 } 908 } 909 910 /** 911 * The action to merge the currently selected layer into another layer. 912 */ 913 public final class DuplicateAction extends AbstractAction implements IEnabledStateUpdating { 914 private transient Layer layer; 915 916 /** 917 * Constructs a new {@code DuplicateAction}. 918 * @param layer the layer 919 * @throws IllegalArgumentException if {@code layer} is null 920 */ 921 public DuplicateAction(Layer layer) { 922 this(); 923 CheckParameterUtil.ensureParameterNotNull(layer, "layer"); 924 this.layer = layer; 925 updateEnabledState(); 926 } 927 928 /** 929 * Constructs a new {@code DuplicateAction}. 930 */ 931 public DuplicateAction() { 932 putValue(NAME, tr("Duplicate")); 933 putValue(SMALL_ICON, ImageProvider.get("dialogs", "duplicatelayer")); 934 putValue(SHORT_DESCRIPTION, tr("Duplicate this layer")); 935 putValue("help", HelpUtil.ht("/Dialog/LayerList#DuplicateLayer")); 936 updateEnabledState(); 937 } 938 939 private void duplicate(Layer layer) { 940 if (!Main.isDisplayingMapView()) 941 return; 942 943 List<String> layerNames = new ArrayList<>(); 944 for (Layer l: Main.map.mapView.getAllLayers()) { 945 layerNames.add(l.getName()); 946 } 947 if (layer instanceof OsmDataLayer) { 948 OsmDataLayer oldLayer = (OsmDataLayer) layer; 949 // Translators: "Copy of {layer name}" 950 String newName = tr("Copy of {0}", oldLayer.getName()); 951 int i = 2; 952 while (layerNames.contains(newName)) { 953 // Translators: "Copy {number} of {layer name}" 954 newName = tr("Copy {1} of {0}", oldLayer.getName(), i); 955 i++; 956 } 957 Main.main.addLayer(new OsmDataLayer(oldLayer.data.clone(), newName, null)); 958 } 959 } 960 961 @Override 962 public void actionPerformed(ActionEvent e) { 963 if (layer != null) { 964 duplicate(layer); 965 } else { 966 duplicate(getModel().getSelectedLayers().get(0)); 967 } 968 } 969 970 @Override 971 public void updateEnabledState() { 972 if (layer == null) { 973 if (getModel().getSelectedLayers().size() == 1) { 974 setEnabled(getModel().getSelectedLayers().get(0) instanceof OsmDataLayer); 975 } else { 976 setEnabled(false); 977 } 978 } else { 979 setEnabled(layer instanceof OsmDataLayer); 980 } 981 } 982 } 983 984 private static class ActiveLayerCheckBox extends JCheckBox { 985 ActiveLayerCheckBox() { 986 setHorizontalAlignment(javax.swing.SwingConstants.CENTER); 987 ImageIcon blank = ImageProvider.get("dialogs/layerlist", "blank"); 988 ImageIcon active = ImageProvider.get("dialogs/layerlist", "active"); 989 setIcon(blank); 990 setSelectedIcon(active); 991 setRolloverIcon(blank); 992 setRolloverSelectedIcon(active); 993 setPressedIcon(ImageProvider.get("dialogs/layerlist", "active-pressed")); 994 } 995 } 996 997 private static class LayerVisibleCheckBox extends JCheckBox { 998 private final ImageIcon iconEye; 999 private final ImageIcon iconEyeTranslucent; 1000 private boolean isTranslucent; 1001 1002 /** 1003 * Constructs a new {@code LayerVisibleCheckBox}. 1004 */ 1005 LayerVisibleCheckBox() { 1006 setHorizontalAlignment(javax.swing.SwingConstants.RIGHT); 1007 iconEye = ImageProvider.get("dialogs/layerlist", "eye"); 1008 iconEyeTranslucent = ImageProvider.get("dialogs/layerlist", "eye-translucent"); 1009 setIcon(ImageProvider.get("dialogs/layerlist", "eye-off")); 1010 setPressedIcon(ImageProvider.get("dialogs/layerlist", "eye-pressed")); 1011 setSelectedIcon(iconEye); 1012 isTranslucent = false; 1013 } 1014 1015 public void setTranslucent(boolean isTranslucent) { 1016 if (this.isTranslucent == isTranslucent) return; 1017 if (isTranslucent) { 1018 setSelectedIcon(iconEyeTranslucent); 1019 } else { 1020 setSelectedIcon(iconEye); 1021 } 1022 this.isTranslucent = isTranslucent; 1023 } 1024 1025 public void updateStatus(Layer layer) { 1026 boolean visible = layer.isVisible(); 1027 setSelected(visible); 1028 setTranslucent(layer.getOpacity() < 1.0); 1029 setToolTipText(visible ? 1030 tr("layer is currently visible (click to hide layer)") : 1031 tr("layer is currently hidden (click to show layer)")); 1032 } 1033 } 1034 1035 private static class NativeScaleLayerCheckBox extends JCheckBox { 1036 NativeScaleLayerCheckBox() { 1037 setHorizontalAlignment(javax.swing.SwingConstants.CENTER); 1038 ImageIcon blank = ImageProvider.get("dialogs/layerlist", "blank"); 1039 ImageIcon active = ImageProvider.get("dialogs/layerlist", "scale"); 1040 setIcon(blank); 1041 setSelectedIcon(active); 1042 } 1043 } 1044 1045 private static class ActiveLayerCellRenderer implements TableCellRenderer { 1046 private final JCheckBox cb; 1047 1048 /** 1049 * Constructs a new {@code ActiveLayerCellRenderer}. 1050 */ 1051 ActiveLayerCellRenderer() { 1052 cb = new ActiveLayerCheckBox(); 1053 } 1054 1055 @Override 1056 public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { 1057 boolean active = value != null && (Boolean) value; 1058 cb.setSelected(active); 1059 cb.setToolTipText(active ? tr("this layer is the active layer") : tr("this layer is not currently active (click to activate)")); 1060 return cb; 1061 } 1062 } 1063 1064 private static class LayerVisibleCellRenderer implements TableCellRenderer { 1065 private final LayerVisibleCheckBox cb; 1066 1067 /** 1068 * Constructs a new {@code LayerVisibleCellRenderer}. 1069 */ 1070 LayerVisibleCellRenderer() { 1071 this.cb = new LayerVisibleCheckBox(); 1072 } 1073 1074 @Override 1075 public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { 1076 if (value != null) { 1077 cb.updateStatus((Layer) value); 1078 } 1079 return cb; 1080 } 1081 } 1082 1083 private static class LayerVisibleCellEditor extends DefaultCellEditor { 1084 private final LayerVisibleCheckBox cb; 1085 1086 LayerVisibleCellEditor(LayerVisibleCheckBox cb) { 1087 super(cb); 1088 this.cb = cb; 1089 } 1090 1091 @Override 1092 public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) { 1093 cb.updateStatus((Layer) value); 1094 return cb; 1095 } 1096 } 1097 1098 private static class NativeScaleLayerCellRenderer implements TableCellRenderer { 1099 private final JCheckBox cb; 1100 1101 /** 1102 * Constructs a new {@code ActiveLayerCellRenderer}. 1103 */ 1104 NativeScaleLayerCellRenderer() { 1105 cb = new NativeScaleLayerCheckBox(); 1106 } 1107 1108 @Override 1109 public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { 1110 Layer layer = (Layer) value; 1111 if (layer instanceof NativeScaleLayer) { 1112 boolean active = ((NativeScaleLayer) layer) == Main.map.mapView.getNativeScaleLayer(); 1113 cb.setSelected(active); 1114 cb.setToolTipText(active 1115 ? tr("scale follows native resolution of this layer") 1116 : tr("scale follows native resolution of another layer (click to set this layer)") 1117 ); 1118 } else { 1119 cb.setSelected(false); 1120 cb.setToolTipText(tr("this layer has no native resolution")); 1121 } 1122 return cb; 1123 } 1124 } 1125 1126 private class LayerNameCellRenderer extends DefaultTableCellRenderer { 1127 1128 protected boolean isActiveLayer(Layer layer) { 1129 if (!Main.isDisplayingMapView()) return false; 1130 return Main.map.mapView.getActiveLayer() == layer; 1131 } 1132 1133 @Override 1134 public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { 1135 if (value == null) 1136 return this; 1137 Layer layer = (Layer) value; 1138 JLabel label = (JLabel) super.getTableCellRendererComponent(table, 1139 layer.getName(), isSelected, hasFocus, row, column); 1140 if (isActiveLayer(layer)) { 1141 label.setFont(label.getFont().deriveFont(Font.BOLD)); 1142 } 1143 if (Main.pref.getBoolean("dialog.layer.colorname", true)) { 1144 Color c = layer.getColor(false); 1145 if (c != null) { 1146 Color oc = null; 1147 for (Layer l : model.getLayers()) { 1148 oc = l.getColor(false); 1149 if (oc != null) { 1150 if (oc.equals(c)) { 1151 oc = null; 1152 } else { 1153 break; 1154 } 1155 } 1156 } 1157 /* not more than one color, don't use coloring */ 1158 if (oc == null) { 1159 c = null; 1160 } 1161 } 1162 if (c == null) { 1163 c = UIManager.getColor(isSelected ? "Table.selectionForeground" : "Table.foreground"); 1164 } 1165 label.setForeground(c); 1166 } 1167 label.setIcon(layer.getIcon()); 1168 label.setToolTipText(layer.getToolTipText()); 1169 return label; 1170 } 1171 } 1172 1173 private static class LayerNameCellEditor extends DefaultCellEditor { 1174 LayerNameCellEditor(DisableShortcutsOnFocusGainedTextField tf) { 1175 super(tf); 1176 } 1177 1178 @Override 1179 public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) { 1180 JosmTextField tf = (JosmTextField) super.getTableCellEditorComponent(table, value, isSelected, row, column); 1181 tf.setText(value == null ? "" : ((Layer) value).getName()); 1182 return tf; 1183 } 1184 } 1185 1186 class PopupMenuHandler extends PopupMenuLauncher { 1187 @Override 1188 public void showMenu(MouseEvent evt) { 1189 menu = new LayerListPopup(getModel().getSelectedLayers()); 1190 super.showMenu(evt); 1191 } 1192 } 1193 1194 /** 1195 * The action to move up the currently selected entries in the list. 1196 */ 1197 class MoveUpAction extends AbstractAction implements IEnabledStateUpdating { 1198 MoveUpAction() { 1199 putValue(NAME, tr("Move up")); 1200 putValue(SMALL_ICON, ImageProvider.get("dialogs", "up")); 1201 putValue(SHORT_DESCRIPTION, tr("Move the selected layer one row up.")); 1202 updateEnabledState(); 1203 } 1204 1205 @Override 1206 public void updateEnabledState() { 1207 setEnabled(model.canMoveUp()); 1208 } 1209 1210 @Override 1211 public void actionPerformed(ActionEvent e) { 1212 model.moveUp(); 1213 } 1214 } 1215 1216 /** 1217 * The action to move down the currently selected entries in the list. 1218 */ 1219 class MoveDownAction extends AbstractAction implements IEnabledStateUpdating { 1220 MoveDownAction() { 1221 putValue(NAME, tr("Move down")); 1222 putValue(SMALL_ICON, ImageProvider.get("dialogs", "down")); 1223 putValue(SHORT_DESCRIPTION, tr("Move the selected layer one row down.")); 1224 updateEnabledState(); 1225 } 1226 1227 @Override 1228 public void updateEnabledState() { 1229 setEnabled(model.canMoveDown()); 1230 } 1231 1232 @Override 1233 public void actionPerformed(ActionEvent e) { 1234 model.moveDown(); 1235 } 1236 } 1237 1238 /** 1239 * Observer interface to be implemented by views using {@link LayerListModel}. 1240 */ 1241 public interface LayerListModelListener { 1242 1243 /** 1244 * Fired when a layer is made visible. 1245 * @param index the layer index 1246 * @param layer the layer 1247 */ 1248 void makeVisible(int index, Layer layer); 1249 1250 1251 /** 1252 * Fired when something has changed in the layer list model. 1253 */ 1254 void refresh(); 1255 } 1256 1257 /** 1258 * The layer list model. The model manages a list of layers and provides methods for 1259 * moving layers up and down, for toggling their visibility, and for activating a layer. 1260 * 1261 * The model is a {@link TableModel} and it provides a {@link ListSelectionModel}. It expects 1262 * to be configured with a {@link DefaultListSelectionModel}. The selection model is used 1263 * to update the selection state of views depending on messages sent to the model. 1264 * 1265 * The model manages a list of {@link LayerListModelListener} which are mainly notified if 1266 * the model requires views to make a specific list entry visible. 1267 * 1268 * It also listens to {@link PropertyChangeEvent}s of every {@link Layer} it manages, in particular to 1269 * the properties {@link Layer#VISIBLE_PROP} and {@link Layer#NAME_PROP}. 1270 */ 1271 public static final class LayerListModel extends AbstractTableModel implements MapView.LayerChangeListener, PropertyChangeListener { 1272 /** manages list selection state*/ 1273 private final DefaultListSelectionModel selectionModel; 1274 private final CopyOnWriteArrayList<LayerListModelListener> listeners; 1275 private LayerList layerList; 1276 1277 /** 1278 * constructor 1279 * 1280 * @param selectionModel the list selection model 1281 */ 1282 LayerListModel(DefaultListSelectionModel selectionModel) { 1283 this.selectionModel = selectionModel; 1284 listeners = new CopyOnWriteArrayList<>(); 1285 } 1286 1287 void setlayerList(LayerList layerList) { 1288 this.layerList = layerList; 1289 } 1290 1291 /** 1292 * Adds a listener to this model 1293 * 1294 * @param listener the listener 1295 */ 1296 public void addLayerListModelListener(LayerListModelListener listener) { 1297 if (listener != null) { 1298 listeners.addIfAbsent(listener); 1299 } 1300 } 1301 1302 /** 1303 * removes a listener from this model 1304 * @param listener the listener 1305 * 1306 */ 1307 public void removeLayerListModelListener(LayerListModelListener listener) { 1308 listeners.remove(listener); 1309 } 1310 1311 /** 1312 * Fires a make visible event to listeners 1313 * 1314 * @param index the index of the row to make visible 1315 * @param layer the layer at this index 1316 * @see LayerListModelListener#makeVisible(int, Layer) 1317 */ 1318 protected void fireMakeVisible(int index, Layer layer) { 1319 for (LayerListModelListener listener : listeners) { 1320 listener.makeVisible(index, layer); 1321 } 1322 } 1323 1324 /** 1325 * Fires a refresh event to listeners of this model 1326 * 1327 * @see LayerListModelListener#refresh() 1328 */ 1329 protected void fireRefresh() { 1330 for (LayerListModelListener listener : listeners) { 1331 listener.refresh(); 1332 } 1333 } 1334 1335 /** 1336 * Populates the model with the current layers managed by {@link MapView}. 1337 */ 1338 public void populate() { 1339 for (Layer layer: getLayers()) { 1340 // make sure the model is registered exactly once 1341 layer.removePropertyChangeListener(this); 1342 layer.addPropertyChangeListener(this); 1343 } 1344 fireTableDataChanged(); 1345 } 1346 1347 /** 1348 * Marks <code>layer</code> as selected layer. Ignored, if layer is null. 1349 * 1350 * @param layer the layer. 1351 */ 1352 public void setSelectedLayer(Layer layer) { 1353 if (layer == null) 1354 return; 1355 int idx = getLayers().indexOf(layer); 1356 if (idx >= 0) { 1357 selectionModel.setSelectionInterval(idx, idx); 1358 } 1359 ensureSelectedIsVisible(); 1360 } 1361 1362 /** 1363 * Replies the list of currently selected layers. Never null, but may be empty. 1364 * 1365 * @return the list of currently selected layers. Never null, but may be empty. 1366 */ 1367 public List<Layer> getSelectedLayers() { 1368 List<Layer> selected = new ArrayList<>(); 1369 List<Layer> layers = getLayers(); 1370 for (int i = 0; i < layers.size(); i++) { 1371 if (selectionModel.isSelectedIndex(i)) { 1372 selected.add(layers.get(i)); 1373 } 1374 } 1375 return selected; 1376 } 1377 1378 /** 1379 * Replies a the list of indices of the selected rows. Never null, but may be empty. 1380 * 1381 * @return the list of indices of the selected rows. Never null, but may be empty. 1382 */ 1383 public List<Integer> getSelectedRows() { 1384 List<Integer> selected = new ArrayList<>(); 1385 for (int i = 0; i < getLayers().size(); i++) { 1386 if (selectionModel.isSelectedIndex(i)) { 1387 selected.add(i); 1388 } 1389 } 1390 return selected; 1391 } 1392 1393 /** 1394 * Invoked if a layer managed by {@link MapView} is removed 1395 * 1396 * @param layer the layer which is removed 1397 */ 1398 protected void onRemoveLayer(Layer layer) { 1399 if (layer == null) 1400 return; 1401 layer.removePropertyChangeListener(this); 1402 final int size = getRowCount(); 1403 final List<Integer> rows = getSelectedRows(); 1404 GuiHelper.runInEDTAndWait(new Runnable() { 1405 @Override 1406 public void run() { 1407 if (rows.isEmpty() && size > 0) { 1408 selectionModel.setSelectionInterval(size-1, size-1); 1409 } 1410 fireTableDataChanged(); 1411 fireRefresh(); 1412 ensureActiveSelected(); 1413 } 1414 }); 1415 } 1416 1417 /** 1418 * Invoked when a layer managed by {@link MapView} is added 1419 * 1420 * @param layer the layer 1421 */ 1422 protected void onAddLayer(Layer layer) { 1423 if (layer == null) return; 1424 layer.addPropertyChangeListener(this); 1425 fireTableDataChanged(); 1426 int idx = getLayers().indexOf(layer); 1427 layerList.setRowHeight(idx, Math.max(16, layer.getIcon().getIconHeight())); 1428 selectionModel.setSelectionInterval(idx, idx); 1429 ensureSelectedIsVisible(); 1430 } 1431 1432 /** 1433 * Replies the first layer. Null if no layers are present 1434 * 1435 * @return the first layer. Null if no layers are present 1436 */ 1437 public Layer getFirstLayer() { 1438 if (getRowCount() == 0) return null; 1439 return getLayers().get(0); 1440 } 1441 1442 /** 1443 * Replies the layer at position <code>index</code> 1444 * 1445 * @param index the index 1446 * @return the layer at position <code>index</code>. Null, 1447 * if index is out of range. 1448 */ 1449 public Layer getLayer(int index) { 1450 if (index < 0 || index >= getRowCount()) 1451 return null; 1452 return getLayers().get(index); 1453 } 1454 1455 /** 1456 * Replies true if the currently selected layers can move up by one position 1457 * 1458 * @return true if the currently selected layers can move up by one position 1459 */ 1460 public boolean canMoveUp() { 1461 List<Integer> sel = getSelectedRows(); 1462 return !sel.isEmpty() && sel.get(0) > 0; 1463 } 1464 1465 /** 1466 * Move up the currently selected layers by one position 1467 * 1468 */ 1469 public void moveUp() { 1470 if (!canMoveUp()) return; 1471 List<Integer> sel = getSelectedRows(); 1472 List<Layer> layers = getLayers(); 1473 for (int row : sel) { 1474 Layer l1 = layers.get(row); 1475 Layer l2 = layers.get(row-1); 1476 Main.map.mapView.moveLayer(l2, row); 1477 Main.map.mapView.moveLayer(l1, row-1); 1478 } 1479 fireTableDataChanged(); 1480 selectionModel.clearSelection(); 1481 for (int row : sel) { 1482 selectionModel.addSelectionInterval(row-1, row-1); 1483 } 1484 ensureSelectedIsVisible(); 1485 } 1486 1487 /** 1488 * Replies true if the currently selected layers can move down by one position 1489 * 1490 * @return true if the currently selected layers can move down by one position 1491 */ 1492 public boolean canMoveDown() { 1493 List<Integer> sel = getSelectedRows(); 1494 return !sel.isEmpty() && sel.get(sel.size()-1) < getLayers().size()-1; 1495 } 1496 1497 /** 1498 * Move down the currently selected layers by one position 1499 * 1500 */ 1501 public void moveDown() { 1502 if (!canMoveDown()) return; 1503 List<Integer> sel = getSelectedRows(); 1504 Collections.reverse(sel); 1505 List<Layer> layers = getLayers(); 1506 for (int row : sel) { 1507 Layer l1 = layers.get(row); 1508 Layer l2 = layers.get(row+1); 1509 Main.map.mapView.moveLayer(l1, row+1); 1510 Main.map.mapView.moveLayer(l2, row); 1511 } 1512 fireTableDataChanged(); 1513 selectionModel.clearSelection(); 1514 for (int row : sel) { 1515 selectionModel.addSelectionInterval(row+1, row+1); 1516 } 1517 ensureSelectedIsVisible(); 1518 } 1519 1520 /** 1521 * Make sure the first of the selected layers is visible in the 1522 * views of this model. 1523 * 1524 */ 1525 protected void ensureSelectedIsVisible() { 1526 int index = selectionModel.getMinSelectionIndex(); 1527 if (index < 0) return; 1528 List<Layer> layers = getLayers(); 1529 if (index >= layers.size()) return; 1530 Layer layer = layers.get(index); 1531 fireMakeVisible(index, layer); 1532 } 1533 1534 /** 1535 * Replies a list of layers which are possible merge targets 1536 * for <code>source</code> 1537 * 1538 * @param source the source layer 1539 * @return a list of layers which are possible merge targets 1540 * for <code>source</code>. Never null, but can be empty. 1541 */ 1542 public List<Layer> getPossibleMergeTargets(Layer source) { 1543 List<Layer> targets = new ArrayList<>(); 1544 if (source == null || !Main.isDisplayingMapView()) { 1545 return targets; 1546 } 1547 for (Layer target : Main.map.mapView.getAllLayersAsList()) { 1548 if (source == target) { 1549 continue; 1550 } 1551 if (target.isMergable(source) && source.isMergable(target)) { 1552 targets.add(target); 1553 } 1554 } 1555 return targets; 1556 } 1557 1558 /** 1559 * Replies the list of layers currently managed by {@link MapView}. 1560 * Never null, but can be empty. 1561 * 1562 * @return the list of layers currently managed by {@link MapView}. 1563 * Never null, but can be empty. 1564 */ 1565 public List<Layer> getLayers() { 1566 if (!Main.isDisplayingMapView()) 1567 return Collections.<Layer>emptyList(); 1568 return Main.map.mapView.getAllLayersAsList(); 1569 } 1570 1571 /** 1572 * Ensures that at least one layer is selected in the layer dialog 1573 * 1574 */ 1575 protected void ensureActiveSelected() { 1576 List<Layer> layers = getLayers(); 1577 if (layers.isEmpty()) 1578 return; 1579 final Layer activeLayer = getActiveLayer(); 1580 if (activeLayer != null) { 1581 // there's an active layer - select it and make it visible 1582 int idx = layers.indexOf(activeLayer); 1583 selectionModel.setSelectionInterval(idx, idx); 1584 ensureSelectedIsVisible(); 1585 } else { 1586 // no active layer - select the first one and make it visible 1587 selectionModel.setSelectionInterval(0, 0); 1588 ensureSelectedIsVisible(); 1589 } 1590 } 1591 1592 /** 1593 * Replies the active layer. null, if no active layer is available 1594 * 1595 * @return the active layer. null, if no active layer is available 1596 */ 1597 protected Layer getActiveLayer() { 1598 if (!Main.isDisplayingMapView()) return null; 1599 return Main.map.mapView.getActiveLayer(); 1600 } 1601 1602 /** 1603 * Replies the scale layer. null, if no active layer is available 1604 * 1605 * @return the scale layer. null, if no active layer is available 1606 */ 1607 protected NativeScaleLayer getNativeScaleLayer() { 1608 if (!Main.isDisplayingMapView()) return null; 1609 return Main.map.mapView.getNativeScaleLayer(); 1610 } 1611 1612 /* ------------------------------------------------------------------------------ */ 1613 /* Interface TableModel */ 1614 /* ------------------------------------------------------------------------------ */ 1615 1616 @Override 1617 public int getRowCount() { 1618 List<Layer> layers = getLayers(); 1619 if (layers == null) return 0; 1620 return layers.size(); 1621 } 1622 1623 @Override 1624 public int getColumnCount() { 1625 return 4; 1626 } 1627 1628 @Override 1629 public Object getValueAt(int row, int col) { 1630 List<Layer> layers = getLayers(); 1631 if (row >= 0 && row < layers.size()) { 1632 switch (col) { 1633 case 0: return layers.get(row) == getActiveLayer(); 1634 case 1: return layers.get(row); 1635 case 2: return layers.get(row); 1636 case 3: return layers.get(row); 1637 default: throw new RuntimeException(); 1638 } 1639 } 1640 return null; 1641 } 1642 1643 @Override 1644 public boolean isCellEditable(int row, int col) { 1645 if (col == 0 && getActiveLayer() == getLayers().get(row)) 1646 return false; 1647 return true; 1648 } 1649 1650 @Override 1651 public void setValueAt(Object value, int row, int col) { 1652 List<Layer> layers = getLayers(); 1653 if (row < layers.size()) { 1654 Layer l = layers.get(row); 1655 switch (col) { 1656 case 0: 1657 Main.map.mapView.setActiveLayer(l); 1658 l.setVisible(true); 1659 break; 1660 case 1: 1661 NativeScaleLayer oldLayer = Main.map.mapView.getNativeScaleLayer(); 1662 if (oldLayer == l) { 1663 Main.map.mapView.setNativeScaleLayer(null); 1664 } else if (l instanceof NativeScaleLayer) { 1665 Main.map.mapView.setNativeScaleLayer((NativeScaleLayer) l); 1666 if (oldLayer != null) { 1667 int idx = getLayers().indexOf(oldLayer); 1668 if (idx >= 0) { 1669 fireTableCellUpdated(idx, col); 1670 } 1671 } 1672 } 1673 break; 1674 case 2: 1675 l.setVisible((Boolean) value); 1676 break; 1677 case 3: 1678 l.rename((String) value); 1679 break; 1680 default: throw new RuntimeException(); 1681 } 1682 fireTableCellUpdated(row, col); 1683 } 1684 } 1685 1686 /* ------------------------------------------------------------------------------ */ 1687 /* Interface LayerChangeListener */ 1688 /* ------------------------------------------------------------------------------ */ 1689 @Override 1690 public void activeLayerChange(final Layer oldLayer, final Layer newLayer) { 1691 GuiHelper.runInEDTAndWait(new Runnable() { 1692 @Override 1693 public void run() { 1694 if (oldLayer != null) { 1695 int idx = getLayers().indexOf(oldLayer); 1696 if (idx >= 0) { 1697 fireTableRowsUpdated(idx, idx); 1698 } 1699 } 1700 1701 if (newLayer != null) { 1702 int idx = getLayers().indexOf(newLayer); 1703 if (idx >= 0) { 1704 fireTableRowsUpdated(idx, idx); 1705 } 1706 } 1707 ensureActiveSelected(); 1708 } 1709 }); 1710 } 1711 1712 @Override 1713 public void layerAdded(Layer newLayer) { 1714 onAddLayer(newLayer); 1715 } 1716 1717 @Override 1718 public void layerRemoved(final Layer oldLayer) { 1719 onRemoveLayer(oldLayer); 1720 } 1721 1722 /* ------------------------------------------------------------------------------ */ 1723 /* Interface PropertyChangeListener */ 1724 /* ------------------------------------------------------------------------------ */ 1725 @Override 1726 public void propertyChange(PropertyChangeEvent evt) { 1727 if (evt.getSource() instanceof Layer) { 1728 Layer layer = (Layer) evt.getSource(); 1729 final int idx = getLayers().indexOf(layer); 1730 if (idx < 0) return; 1731 fireRefresh(); 1732 } 1733 } 1734 } 1735 1736 static class LayerList extends JTable { 1737 LayerList(LayerListModel dataModel) { 1738 super(dataModel); 1739 dataModel.setlayerList(this); 1740 } 1741 1742 public void scrollToVisible(int row, int col) { 1743 if (!(getParent() instanceof JViewport)) 1744 return; 1745 JViewport viewport = (JViewport) getParent(); 1746 Rectangle rect = getCellRect(row, col, true); 1747 Point pt = viewport.getViewPosition(); 1748 rect.setLocation(rect.x - pt.x, rect.y - pt.y); 1749 viewport.scrollRectToVisible(rect); 1750 } 1751 } 1752 1753 /** 1754 * Creates a {@link ShowHideLayerAction} in the 1755 * context of this {@link LayerListDialog}. 1756 * 1757 * @return the action 1758 */ 1759 public ShowHideLayerAction createShowHideLayerAction() { 1760 return new ShowHideLayerAction(); 1761 } 1762 1763 /** 1764 * Creates a {@link DeleteLayerAction} in the 1765 * context of this {@link LayerListDialog}. 1766 * 1767 * @return the action 1768 */ 1769 public DeleteLayerAction createDeleteLayerAction() { 1770 return new DeleteLayerAction(); 1771 } 1772 1773 /** 1774 * Creates a {@link ActivateLayerAction} for <code>layer</code> in the 1775 * context of this {@link LayerListDialog}. 1776 * 1777 * @param layer the layer 1778 * @return the action 1779 */ 1780 public ActivateLayerAction createActivateLayerAction(Layer layer) { 1781 return new ActivateLayerAction(layer); 1782 } 1783 1784 /** 1785 * Creates a {@link MergeLayerAction} for <code>layer</code> in the 1786 * context of this {@link LayerListDialog}. 1787 * 1788 * @param layer the layer 1789 * @return the action 1790 */ 1791 public MergeAction createMergeLayerAction(Layer layer) { 1792 return new MergeAction(layer); 1793 } 1794 1795 /** 1796 * Creates a {@link DuplicateAction} for <code>layer</code> in the 1797 * context of this {@link LayerListDialog}. 1798 * 1799 * @param layer the layer 1800 * @return the action 1801 */ 1802 public DuplicateAction createDuplicateLayerAction(Layer layer) { 1803 return new DuplicateAction(layer); 1804 } 1805 1806 /** 1807 * Returns the layer at given index, or {@code null}. 1808 * @param index the index 1809 * @return the layer at given index, or {@code null} if index out of range 1810 */ 1811 public static Layer getLayerForIndex(int index) { 1812 1813 if (!Main.isDisplayingMapView()) 1814 return null; 1815 1816 List<Layer> layers = Main.map.mapView.getAllLayersAsList(); 1817 1818 if (index < layers.size() && index >= 0) 1819 return layers.get(index); 1820 else 1821 return null; 1822 } 1823 1824 /** 1825 * Returns a list of info on all layers of a given class. 1826 * @param layerClass The layer class. This is not {@code Class<? extends Layer>} on purpose, 1827 * to allow asking for layers implementing some interface 1828 * @return list of info on all layers assignable from {@code layerClass} 1829 */ 1830 public static List<MultikeyInfo> getLayerInfoByClass(Class<?> layerClass) { 1831 1832 List<MultikeyInfo> result = new ArrayList<>(); 1833 1834 if (!Main.isDisplayingMapView()) 1835 return result; 1836 1837 List<Layer> layers = Main.map.mapView.getAllLayersAsList(); 1838 1839 int index = 0; 1840 for (Layer l: layers) { 1841 if (layerClass.isAssignableFrom(l.getClass())) { 1842 result.add(new MultikeyInfo(index, l.getName())); 1843 } 1844 index++; 1845 } 1846 1847 return result; 1848 } 1849 1850 /** 1851 * Determines if a layer is valid (contained in layer list). 1852 * @param l the layer 1853 * @return {@code true} if layer {@code l} is contained in current layer list 1854 */ 1855 public static boolean isLayerValid(Layer l) { 1856 1857 if (l == null || !Main.isDisplayingMapView()) 1858 return false; 1859 1860 return Main.map.mapView.getAllLayersAsList().contains(l); 1861 } 1862 1863 /** 1864 * Returns info about layer. 1865 * @param l the layer 1866 * @return info about layer {@code l} 1867 */ 1868 public static MultikeyInfo getLayerInfo(Layer l) { 1869 1870 if (l == null || !Main.isDisplayingMapView()) 1871 return null; 1872 1873 int index = Main.map.mapView.getAllLayersAsList().indexOf(l); 1874 if (index < 0) 1875 return null; 1876 1877 return new MultikeyInfo(index, l.getName()); 1878 } 1879}