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