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