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.BorderLayout; 007import java.awt.Component; 008import java.awt.event.ActionEvent; 009import java.awt.event.KeyEvent; 010import java.awt.event.MouseEvent; 011import java.beans.PropertyChangeEvent; 012import java.beans.PropertyChangeListener; 013import java.util.ArrayList; 014import java.util.Arrays; 015import java.util.Collection; 016import java.util.Collections; 017import java.util.HashSet; 018import java.util.List; 019import java.util.Set; 020 021import javax.swing.AbstractAction; 022import javax.swing.AbstractListModel; 023import javax.swing.DefaultListSelectionModel; 024import javax.swing.FocusManager; 025import javax.swing.JComponent; 026import javax.swing.JList; 027import javax.swing.JPanel; 028import javax.swing.JPopupMenu; 029import javax.swing.JScrollPane; 030import javax.swing.KeyStroke; 031import javax.swing.ListSelectionModel; 032import javax.swing.event.ListSelectionEvent; 033import javax.swing.event.ListSelectionListener; 034 035import org.openstreetmap.josm.Main; 036import org.openstreetmap.josm.actions.relation.AddSelectionToRelations; 037import org.openstreetmap.josm.actions.relation.DeleteRelationsAction; 038import org.openstreetmap.josm.actions.relation.DownloadMembersAction; 039import org.openstreetmap.josm.actions.relation.DownloadSelectedIncompleteMembersAction; 040import org.openstreetmap.josm.actions.relation.DuplicateRelationAction; 041import org.openstreetmap.josm.actions.relation.EditRelationAction; 042import org.openstreetmap.josm.actions.relation.SelectMembersAction; 043import org.openstreetmap.josm.actions.relation.SelectRelationAction; 044import org.openstreetmap.josm.actions.search.SearchCompiler; 045import org.openstreetmap.josm.data.osm.DataSet; 046import org.openstreetmap.josm.data.osm.OsmPrimitive; 047import org.openstreetmap.josm.data.osm.Relation; 048import org.openstreetmap.josm.data.osm.event.AbstractDatasetChangedEvent; 049import org.openstreetmap.josm.data.osm.event.DataChangedEvent; 050import org.openstreetmap.josm.data.osm.event.DataSetListener; 051import org.openstreetmap.josm.data.osm.event.DatasetEventManager; 052import org.openstreetmap.josm.data.osm.event.DatasetEventManager.FireMode; 053import org.openstreetmap.josm.data.osm.event.NodeMovedEvent; 054import org.openstreetmap.josm.data.osm.event.PrimitivesAddedEvent; 055import org.openstreetmap.josm.data.osm.event.PrimitivesRemovedEvent; 056import org.openstreetmap.josm.data.osm.event.RelationMembersChangedEvent; 057import org.openstreetmap.josm.data.osm.event.TagsChangedEvent; 058import org.openstreetmap.josm.data.osm.event.WayNodesChangedEvent; 059import org.openstreetmap.josm.gui.DefaultNameFormatter; 060import org.openstreetmap.josm.gui.MapView; 061import org.openstreetmap.josm.gui.MapView.LayerChangeListener; 062import org.openstreetmap.josm.gui.NavigatableComponent; 063import org.openstreetmap.josm.gui.OsmPrimitivRenderer; 064import org.openstreetmap.josm.gui.PopupMenuHandler; 065import org.openstreetmap.josm.gui.SideButton; 066import org.openstreetmap.josm.gui.dialogs.relation.RelationEditor; 067import org.openstreetmap.josm.gui.layer.Layer; 068import org.openstreetmap.josm.gui.layer.OsmDataLayer; 069import org.openstreetmap.josm.gui.util.GuiHelper; 070import org.openstreetmap.josm.gui.util.HighlightHelper; 071import org.openstreetmap.josm.gui.widgets.CompileSearchTextDecorator; 072import org.openstreetmap.josm.gui.widgets.DisableShortcutsOnFocusGainedTextField; 073import org.openstreetmap.josm.gui.widgets.JosmTextField; 074import org.openstreetmap.josm.gui.widgets.PopupMenuLauncher; 075import org.openstreetmap.josm.tools.ImageProvider; 076import org.openstreetmap.josm.tools.InputMapUtils; 077import org.openstreetmap.josm.tools.Predicate; 078import org.openstreetmap.josm.tools.Shortcut; 079import org.openstreetmap.josm.tools.Utils; 080 081/** 082 * A dialog showing all known relations, with buttons to add, edit, and 083 * delete them. 084 * 085 * We don't have such dialogs for nodes, segments, and ways, because those 086 * objects are visible on the map and can be selected there. Relations are not. 087 */ 088public class RelationListDialog extends ToggleDialog implements DataSetListener, NavigatableComponent.ZoomChangeListener { 089 /** The display list. */ 090 private final JList<Relation> displaylist; 091 /** the list model used */ 092 private final RelationListModel model; 093 094 private final NewAction newAction; 095 096 /** the popup menu and its handler */ 097 private final JPopupMenu popupMenu = new JPopupMenu(); 098 private final transient PopupMenuHandler popupMenuHandler = new PopupMenuHandler(popupMenu); 099 100 private final JosmTextField filter; 101 102 // Actions 103 /** the edit action */ 104 private final EditRelationAction editAction = new EditRelationAction(); 105 /** the delete action */ 106 private final DeleteRelationsAction deleteRelationsAction = new DeleteRelationsAction(); 107 /** the duplicate action */ 108 private final DuplicateRelationAction duplicateAction = new DuplicateRelationAction(); 109 private final DownloadMembersAction downloadMembersAction = new DownloadMembersAction(); 110 private final DownloadSelectedIncompleteMembersAction downloadSelectedIncompleteMembersAction = 111 new DownloadSelectedIncompleteMembersAction(); 112 private final SelectMembersAction selectMembersAction = new SelectMembersAction(false); 113 private final SelectMembersAction addMembersToSelectionAction = new SelectMembersAction(true); 114 private final SelectRelationAction selectRelationAction = new SelectRelationAction(false); 115 private final SelectRelationAction addRelationToSelectionAction = new SelectRelationAction(true); 116 /** add all selected primitives to the given relations */ 117 private final AddSelectionToRelations addSelectionToRelations = new AddSelectionToRelations(); 118 119 private final transient HighlightHelper highlightHelper = new HighlightHelper(); 120 private final boolean highlightEnabled = Main.pref.getBoolean("draw.target-highlight", true); 121 122 /** 123 * Constructs <code>RelationListDialog</code> 124 */ 125 public RelationListDialog() { 126 super(tr("Relations"), "relationlist", tr("Open a list of all relations."), 127 Shortcut.registerShortcut("subwindow:relations", tr("Toggle: {0}", tr("Relations")), 128 KeyEvent.VK_R, Shortcut.ALT_SHIFT), 150, true); 129 130 // create the list of relations 131 // 132 DefaultListSelectionModel selectionModel = new DefaultListSelectionModel(); 133 model = new RelationListModel(selectionModel); 134 displaylist = new JList<>(model); 135 displaylist.setSelectionModel(selectionModel); 136 displaylist.setCellRenderer(new OsmPrimitivRenderer() { 137 /** 138 * Don't show the default tooltip in the relation list. 139 */ 140 @Override 141 protected String getComponentToolTipText(OsmPrimitive value) { 142 return null; 143 } 144 }); 145 displaylist.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); 146 displaylist.addMouseListener(new MouseEventHandler()); 147 148 // the new action 149 // 150 newAction = new NewAction(); 151 152 filter = setupFilter(); 153 154 displaylist.addListSelectionListener(new ListSelectionListener() { 155 @Override 156 public void valueChanged(ListSelectionEvent e) { 157 updateActionsRelationLists(); 158 } 159 }); 160 161 // Setup popup menu handler 162 setupPopupMenuHandler(); 163 164 JPanel pane = new JPanel(new BorderLayout()); 165 pane.add(filter, BorderLayout.NORTH); 166 pane.add(new JScrollPane(displaylist), BorderLayout.CENTER); 167 createLayout(pane, false, Arrays.asList(new SideButton[]{ 168 new SideButton(newAction, false), 169 new SideButton(editAction, false), 170 new SideButton(duplicateAction, false), 171 new SideButton(deleteRelationsAction, false), 172 new SideButton(selectRelationAction, false) 173 })); 174 175 InputMapUtils.unassignCtrlShiftUpDown(displaylist, JComponent.WHEN_FOCUSED); 176 177 // Select relation on Enter 178 InputMapUtils.addEnterAction(displaylist, selectRelationAction); 179 180 // Edit relation on Ctrl-Enter 181 displaylist.getActionMap().put("edit", editAction); 182 displaylist.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, KeyEvent.CTRL_MASK), "edit"); 183 184 // Do not hide copy action because of default JList override (fix #9815) 185 displaylist.getActionMap().put("copy", Main.main.menu.copy); 186 displaylist.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_C, GuiHelper.getMenuShortcutKeyMaskEx()), "copy"); 187 188 updateActionsRelationLists(); 189 } 190 191 // inform all actions about list of relations they need 192 private void updateActionsRelationLists() { 193 List<Relation> sel = model.getSelectedRelations(); 194 popupMenuHandler.setPrimitives(sel); 195 196 Component focused = FocusManager.getCurrentKeyboardFocusManager().getFocusOwner(); 197 198 //update highlights 199 if (highlightEnabled && focused == displaylist && Main.isDisplayingMapView()) { 200 if (highlightHelper.highlightOnly(sel)) { 201 Main.map.mapView.repaint(); 202 } 203 } 204 } 205 206 @Override 207 public void showNotify() { 208 MapView.addLayerChangeListener(newAction); 209 MapView.addZoomChangeListener(this); 210 newAction.updateEnabledState(); 211 DatasetEventManager.getInstance().addDatasetListener(this, FireMode.IN_EDT); 212 DataSet.addSelectionListener(addSelectionToRelations); 213 dataChanged(null); 214 } 215 216 @Override 217 public void hideNotify() { 218 MapView.removeLayerChangeListener(newAction); 219 MapView.removeZoomChangeListener(this); 220 DatasetEventManager.getInstance().removeDatasetListener(this); 221 DataSet.removeSelectionListener(addSelectionToRelations); 222 } 223 224 private void resetFilter() { 225 filter.setText(null); 226 } 227 228 /** 229 * Initializes the relation list dialog from a layer. If <code>layer</code> is null 230 * or if it isn't an {@link OsmDataLayer} the dialog is reset to an empty dialog. 231 * Otherwise it is initialized with the list of non-deleted and visible relations 232 * in the layer's dataset. 233 * 234 * @param layer the layer. May be null. 235 */ 236 protected void initFromLayer(Layer layer) { 237 if (!(layer instanceof OsmDataLayer)) { 238 model.setRelations(null); 239 return; 240 } 241 OsmDataLayer l = (OsmDataLayer) layer; 242 model.setRelations(l.data.getRelations()); 243 model.updateTitle(); 244 updateActionsRelationLists(); 245 } 246 247 /** 248 * @return The selected relation in the list 249 */ 250 private Relation getSelected() { 251 if (model.getSize() == 1) { 252 displaylist.setSelectedIndex(0); 253 } 254 return displaylist.getSelectedValue(); 255 } 256 257 /** 258 * Selects the relation <code>relation</code> in the list of relations. 259 * 260 * @param relation the relation 261 */ 262 public void selectRelation(Relation relation) { 263 selectRelations(Collections.singleton(relation)); 264 } 265 266 /** 267 * Selects the relations in the list of relations. 268 * @param relations the relations to be selected 269 */ 270 public void selectRelations(Collection<Relation> relations) { 271 if (relations == null || relations.isEmpty()) { 272 model.setSelectedRelations(null); 273 } else { 274 model.setSelectedRelations(relations); 275 Integer i = model.getVisibleRelationIndex(relations.iterator().next()); 276 if (i != null) { 277 // Not all relations have to be in the list 278 // (for example when the relation list is hidden, it's not updated with new relations) 279 displaylist.scrollRectToVisible(displaylist.getCellBounds(i, i)); 280 } 281 } 282 } 283 284 private JosmTextField setupFilter() { 285 final JosmTextField f = new DisableShortcutsOnFocusGainedTextField(); 286 f.setToolTipText(tr("Relation list filter")); 287 final CompileSearchTextDecorator decorator = CompileSearchTextDecorator.decorate(f); 288 f.addPropertyChangeListener("filter", new PropertyChangeListener() { 289 @Override 290 public void propertyChange(PropertyChangeEvent evt) { 291 model.setFilter(decorator.getMatch()); 292 } 293 }); 294 return f; 295 } 296 297 class MouseEventHandler extends PopupMenuLauncher { 298 299 MouseEventHandler() { 300 super(popupMenu); 301 } 302 303 @Override 304 public void mouseExited(MouseEvent me) { 305 if (highlightEnabled) highlightHelper.clear(); 306 } 307 308 protected void setCurrentRelationAsSelection() { 309 Main.main.getCurrentDataSet().setSelected(displaylist.getSelectedValue()); 310 } 311 312 protected void editCurrentRelation() { 313 EditRelationAction.launchEditor(getSelected()); 314 } 315 316 @Override 317 public void mouseClicked(MouseEvent e) { 318 if (!Main.main.hasEditLayer()) return; 319 if (isDoubleClick(e)) { 320 if (e.isControlDown()) { 321 editCurrentRelation(); 322 } else { 323 setCurrentRelationAsSelection(); 324 } 325 } 326 } 327 } 328 329 /** 330 * The action for creating a new relation 331 * 332 */ 333 static class NewAction extends AbstractAction implements LayerChangeListener { 334 NewAction() { 335 putValue(SHORT_DESCRIPTION, tr("Create a new relation")); 336 putValue(NAME, tr("New")); 337 putValue(SMALL_ICON, ImageProvider.get("dialogs", "addrelation")); 338 updateEnabledState(); 339 } 340 341 public void run() { 342 RelationEditor.getEditor(Main.main.getEditLayer(), null, null).setVisible(true); 343 } 344 345 @Override 346 public void actionPerformed(ActionEvent e) { 347 run(); 348 } 349 350 protected void updateEnabledState() { 351 setEnabled(Main.main != null && Main.main.hasEditLayer()); 352 } 353 354 @Override 355 public void activeLayerChange(Layer oldLayer, Layer newLayer) { 356 updateEnabledState(); 357 } 358 359 @Override 360 public void layerAdded(Layer newLayer) { 361 updateEnabledState(); 362 } 363 364 @Override 365 public void layerRemoved(Layer oldLayer) { 366 updateEnabledState(); 367 } 368 } 369 370 /** 371 * The list model for the list of relations displayed in the relation list dialog. 372 * 373 */ 374 private class RelationListModel extends AbstractListModel<Relation> { 375 private final transient List<Relation> relations = new ArrayList<>(); 376 private transient List<Relation> filteredRelations; 377 private final DefaultListSelectionModel selectionModel; 378 private transient SearchCompiler.Match filter; 379 380 RelationListModel(DefaultListSelectionModel selectionModel) { 381 this.selectionModel = selectionModel; 382 } 383 384 public void sort() { 385 Collections.sort( 386 relations, 387 DefaultNameFormatter.getInstance().getRelationComparator() 388 ); 389 } 390 391 private boolean isValid(Relation r) { 392 return !r.isDeleted() && r.isVisible() && !r.isIncomplete(); 393 } 394 395 public void setRelations(Collection<Relation> relations) { 396 List<Relation> sel = getSelectedRelations(); 397 this.relations.clear(); 398 this.filteredRelations = null; 399 if (relations == null) { 400 selectionModel.clearSelection(); 401 fireContentsChanged(this, 0, getSize()); 402 return; 403 } 404 for (Relation r: relations) { 405 if (isValid(r)) { 406 this.relations.add(r); 407 } 408 } 409 sort(); 410 updateFilteredRelations(); 411 fireIntervalAdded(this, 0, getSize()); 412 setSelectedRelations(sel); 413 } 414 415 /** 416 * Add all relations in <code>addedPrimitives</code> to the model for the 417 * relation list dialog 418 * 419 * @param addedPrimitives the collection of added primitives. May include nodes, 420 * ways, and relations. 421 */ 422 public void addRelations(Collection<? extends OsmPrimitive> addedPrimitives) { 423 boolean added = false; 424 for (OsmPrimitive p: addedPrimitives) { 425 if (!(p instanceof Relation)) { 426 continue; 427 } 428 429 Relation r = (Relation) p; 430 if (relations.contains(r)) { 431 continue; 432 } 433 if (isValid(r)) { 434 relations.add(r); 435 added = true; 436 } 437 } 438 if (added) { 439 List<Relation> sel = getSelectedRelations(); 440 sort(); 441 updateFilteredRelations(); 442 fireIntervalAdded(this, 0, getSize()); 443 setSelectedRelations(sel); 444 } 445 } 446 447 /** 448 * Removes all relations in <code>removedPrimitives</code> from the model 449 * 450 * @param removedPrimitives the removed primitives. May include nodes, ways, 451 * and relations 452 */ 453 public void removeRelations(Collection<? extends OsmPrimitive> removedPrimitives) { 454 if (removedPrimitives == null) return; 455 // extract the removed relations 456 // 457 Set<Relation> removedRelations = new HashSet<>(); 458 for (OsmPrimitive p: removedPrimitives) { 459 if (!(p instanceof Relation)) { 460 continue; 461 } 462 removedRelations.add((Relation) p); 463 } 464 if (removedRelations.isEmpty()) 465 return; 466 int size = relations.size(); 467 relations.removeAll(removedRelations); 468 if (filteredRelations != null) { 469 filteredRelations.removeAll(removedRelations); 470 } 471 if (size != relations.size()) { 472 List<Relation> sel = getSelectedRelations(); 473 sort(); 474 fireContentsChanged(this, 0, getSize()); 475 setSelectedRelations(sel); 476 } 477 } 478 479 private void updateFilteredRelations() { 480 if (filter != null) { 481 filteredRelations = new ArrayList<>(Utils.filter(relations, new Predicate<Relation>() { 482 @Override 483 public boolean evaluate(Relation r) { 484 return filter.match(r); 485 } 486 })); 487 } else if (filteredRelations != null) { 488 filteredRelations = null; 489 } 490 } 491 492 public void setFilter(final SearchCompiler.Match filter) { 493 this.filter = filter; 494 updateFilteredRelations(); 495 List<Relation> sel = getSelectedRelations(); 496 fireContentsChanged(this, 0, getSize()); 497 setSelectedRelations(sel); 498 updateTitle(); 499 } 500 501 private List<Relation> getVisibleRelations() { 502 return filteredRelations == null ? relations : filteredRelations; 503 } 504 505 private Relation getVisibleRelation(int index) { 506 if (index < 0 || index >= getVisibleRelations().size()) return null; 507 return getVisibleRelations().get(index); 508 } 509 510 @Override 511 public Relation getElementAt(int index) { 512 return getVisibleRelation(index); 513 } 514 515 @Override 516 public int getSize() { 517 return getVisibleRelations().size(); 518 } 519 520 /** 521 * Replies the list of selected relations. Empty list, 522 * if there are no selected relations. 523 * 524 * @return the list of selected, non-new relations. 525 */ 526 public List<Relation> getSelectedRelations() { 527 List<Relation> ret = new ArrayList<>(); 528 for (int i = 0; i < getSize(); i++) { 529 if (!selectionModel.isSelectedIndex(i)) { 530 continue; 531 } 532 ret.add(getVisibleRelation(i)); 533 } 534 return ret; 535 } 536 537 /** 538 * Sets the selected relations. 539 * 540 * @param sel the list of selected relations 541 */ 542 public void setSelectedRelations(Collection<Relation> sel) { 543 selectionModel.clearSelection(); 544 if (sel == null || sel.isEmpty()) 545 return; 546 if (!getVisibleRelations().containsAll(sel)) { 547 resetFilter(); 548 } 549 for (Relation r: sel) { 550 Integer i = getVisibleRelationIndex(r); 551 if (i != null) { 552 selectionModel.addSelectionInterval(i, i); 553 } 554 } 555 } 556 557 private Integer getVisibleRelationIndex(Relation rel) { 558 int i = getVisibleRelations().indexOf(rel); 559 if (i < 0) 560 return null; 561 return i; 562 } 563 564 public void updateTitle() { 565 if (!relations.isEmpty() && relations.size() != getSize()) { 566 RelationListDialog.this.setTitle(tr("Relations: {0}/{1}", getSize(), relations.size())); 567 } else if (getSize() > 0) { 568 RelationListDialog.this.setTitle(tr("Relations: {0}", getSize())); 569 } else { 570 RelationListDialog.this.setTitle(tr("Relations")); 571 } 572 } 573 } 574 575 private void setupPopupMenuHandler() { 576 577 // -- select action 578 popupMenuHandler.addAction(selectRelationAction); 579 popupMenuHandler.addAction(addRelationToSelectionAction); 580 581 // -- select members action 582 popupMenuHandler.addAction(selectMembersAction); 583 popupMenuHandler.addAction(addMembersToSelectionAction); 584 585 popupMenuHandler.addSeparator(); 586 // -- download members action 587 popupMenuHandler.addAction(downloadMembersAction); 588 589 // -- download incomplete members action 590 popupMenuHandler.addAction(downloadSelectedIncompleteMembersAction); 591 592 popupMenuHandler.addSeparator(); 593 popupMenuHandler.addAction(editAction).setVisible(false); 594 popupMenuHandler.addAction(duplicateAction).setVisible(false); 595 popupMenuHandler.addAction(deleteRelationsAction).setVisible(false); 596 597 popupMenuHandler.addAction(addSelectionToRelations); 598 } 599 600 /* ---------------------------------------------------------------------------------- */ 601 /* Methods that can be called from plugins */ 602 /* ---------------------------------------------------------------------------------- */ 603 604 /** 605 * Replies the popup menu handler. 606 * @return The popup menu handler 607 */ 608 public PopupMenuHandler getPopupMenuHandler() { 609 return popupMenuHandler; 610 } 611 612 /** 613 * Replies the list of selected relations. Empty list, if there are no selected relations. 614 * @return the list of selected, non-new relations. 615 */ 616 public Collection<Relation> getSelectedRelations() { 617 return model.getSelectedRelations(); 618 } 619 620 /* ---------------------------------------------------------------------------------- */ 621 /* DataSetListener */ 622 /* ---------------------------------------------------------------------------------- */ 623 624 @Override 625 public void nodeMoved(NodeMovedEvent event) { 626 /* irrelevant in this context */ 627 } 628 629 @Override 630 public void wayNodesChanged(WayNodesChangedEvent event) { 631 /* irrelevant in this context */ 632 } 633 634 @Override 635 public void primitivesAdded(final PrimitivesAddedEvent event) { 636 model.addRelations(event.getPrimitives()); 637 model.updateTitle(); 638 } 639 640 @Override 641 public void primitivesRemoved(final PrimitivesRemovedEvent event) { 642 model.removeRelations(event.getPrimitives()); 643 model.updateTitle(); 644 } 645 646 @Override 647 public void relationMembersChanged(final RelationMembersChangedEvent event) { 648 List<Relation> sel = model.getSelectedRelations(); 649 model.sort(); 650 model.setSelectedRelations(sel); 651 displaylist.repaint(); 652 } 653 654 @Override 655 public void tagsChanged(TagsChangedEvent event) { 656 OsmPrimitive prim = event.getPrimitive(); 657 if (!(prim instanceof Relation)) 658 return; 659 // trigger a sort of the relation list because the display name may have changed 660 // 661 List<Relation> sel = model.getSelectedRelations(); 662 model.sort(); 663 model.setSelectedRelations(sel); 664 displaylist.repaint(); 665 } 666 667 @Override 668 public void dataChanged(DataChangedEvent event) { 669 initFromLayer(Main.main.getEditLayer()); 670 } 671 672 @Override 673 public void otherDatasetChange(AbstractDatasetChangedEvent event) { 674 /* ignore */ 675 } 676 677 @Override 678 public void zoomChanged() { 679 // re-filter relations 680 if (model.filter != null) { 681 model.setFilter(model.filter); 682 } 683 } 684}