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<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 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, true); 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 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(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 dialog. 380 * 381 */ 382 private class RelationListModel extends AbstractListModel<Relation> { 383 private final List<Relation> relations = new ArrayList<>(); 384 private List<Relation> filteredRelations; 385 private DefaultListSelectionModel selectionModel; 386 private SearchCompiler.Match filter; 387 388 public RelationListModel(DefaultListSelectionModel selectionModel) { 389 this.selectionModel = selectionModel; 390 } 391 392 public void sort() { 393 Collections.sort( 394 relations, 395 DefaultNameFormatter.getInstance().getRelationComparator() 396 ); 397 } 398 399 private boolean isValid(Relation r) { 400 return !r.isDeleted() && r.isVisible() && !r.isIncomplete(); 401 } 402 403 public void setRelations(Collection<Relation> relations) { 404 List<Relation> sel = getSelectedRelations(); 405 this.relations.clear(); 406 this.filteredRelations = null; 407 if (relations == null) { 408 selectionModel.clearSelection(); 409 fireContentsChanged(this,0,getSize()); 410 return; 411 } 412 for (Relation r: relations) { 413 if (isValid(r)) { 414 this.relations.add(r); 415 } 416 } 417 sort(); 418 updateFilteredRelations(); 419 fireIntervalAdded(this, 0, getSize()); 420 setSelectedRelations(sel); 421 } 422 423 /** 424 * Add all relations in <code>addedPrimitives</code> to the model for the 425 * relation list dialog 426 * 427 * @param addedPrimitives the collection of added primitives. May include nodes, 428 * ways, and relations. 429 */ 430 public void addRelations(Collection<? extends OsmPrimitive> addedPrimitives) { 431 boolean added = false; 432 for (OsmPrimitive p: addedPrimitives) { 433 if (! (p instanceof Relation)) { 434 continue; 435 } 436 437 Relation r = (Relation)p; 438 if (relations.contains(r)) { 439 continue; 440 } 441 if (isValid(r)) { 442 relations.add(r); 443 added = true; 444 } 445 } 446 if (added) { 447 List<Relation> sel = getSelectedRelations(); 448 sort(); 449 updateFilteredRelations(); 450 fireIntervalAdded(this, 0, getSize()); 451 setSelectedRelations(sel); 452 } 453 } 454 455 /** 456 * Removes all relations in <code>removedPrimitives</code> from the model 457 * 458 * @param removedPrimitives the removed primitives. May include nodes, ways, 459 * and relations 460 */ 461 public void removeRelations(Collection<? extends OsmPrimitive> removedPrimitives) { 462 if (removedPrimitives == null) return; 463 // extract the removed relations 464 // 465 Set<Relation> removedRelations = new HashSet<>(); 466 for (OsmPrimitive p: removedPrimitives) { 467 if (! (p instanceof Relation)) { 468 continue; 469 } 470 removedRelations.add((Relation)p); 471 } 472 if (removedRelations.isEmpty()) 473 return; 474 int size = relations.size(); 475 relations.removeAll(removedRelations); 476 if (filteredRelations != null) { 477 filteredRelations.removeAll(removedRelations); 478 } 479 if (size != relations.size()) { 480 List<Relation> sel = getSelectedRelations(); 481 sort(); 482 fireContentsChanged(this, 0, getSize()); 483 setSelectedRelations(sel); 484 } 485 } 486 487 private void updateFilteredRelations() { 488 if (filter != null) { 489 filteredRelations = new ArrayList<>(Utils.filter(relations, new Predicate<Relation>() { 490 @Override 491 public boolean evaluate(Relation r) { 492 return filter.match(r); 493 } 494 })); 495 } else if (filteredRelations != null) { 496 filteredRelations = null; 497 } 498 } 499 500 public void setFilter(final SearchCompiler.Match filter) { 501 this.filter = filter; 502 updateFilteredRelations(); 503 List<Relation> sel = getSelectedRelations(); 504 fireContentsChanged(this, 0, getSize()); 505 setSelectedRelations(sel); 506 updateTitle(); 507 } 508 509 private List<Relation> getVisibleRelations() { 510 return filteredRelations == null ? relations : filteredRelations; 511 } 512 513 private Relation getVisibleRelation(int index) { 514 if (index < 0 || index >= getVisibleRelations().size()) return null; 515 return getVisibleRelations().get(index); 516 } 517 518 @Override 519 public Relation getElementAt(int index) { 520 return getVisibleRelation(index); 521 } 522 523 @Override 524 public int getSize() { 525 return getVisibleRelations().size(); 526 } 527 528 /** 529 * Replies the list of selected relations. Empty list, 530 * if there are no selected relations. 531 * 532 * @return the list of selected, non-new relations. 533 */ 534 public List<Relation> getSelectedRelations() { 535 List<Relation> ret = new ArrayList<>(); 536 for (int i=0; i<getSize();i++) { 537 if (!selectionModel.isSelectedIndex(i)) { 538 continue; 539 } 540 ret.add(getVisibleRelation(i)); 541 } 542 return ret; 543 } 544 545 /** 546 * Sets the selected relations. 547 * 548 * @param sel the list of selected relations 549 */ 550 public void setSelectedRelations(Collection<Relation> sel) { 551 selectionModel.clearSelection(); 552 if (sel == null || sel.isEmpty()) 553 return; 554 if (!getVisibleRelations().containsAll(sel)) { 555 resetFilter(); 556 } 557 for (Relation r: sel) { 558 Integer i = getVisibleRelationIndex(r); 559 if (i != null) { 560 selectionModel.addSelectionInterval(i,i); 561 } 562 } 563 } 564 565 private Integer getVisibleRelationIndex(Relation rel) { 566 int i = getVisibleRelations().indexOf(rel); 567 if (i<0) 568 return null; 569 return i; 570 } 571 572 public void updateTitle() { 573 if (relations.size() > 0 && relations.size() != getSize()) { 574 RelationListDialog.this.setTitle(tr("Relations: {0}/{1}", getSize(), relations.size())); 575 } else if (getSize() > 0) { 576 RelationListDialog.this.setTitle(tr("Relations: {0}", getSize())); 577 } else { 578 RelationListDialog.this.setTitle(tr("Relations")); 579 } 580 } 581 } 582 583 private final void setupPopupMenuHandler() { 584 585 // -- select action 586 popupMenuHandler.addAction(selectRelationAction); 587 popupMenuHandler.addAction(addRelationToSelectionAction); 588 589 // -- select members action 590 popupMenuHandler.addAction(selectMembersAction); 591 popupMenuHandler.addAction(addMembersToSelectionAction); 592 593 popupMenuHandler.addSeparator(); 594 // -- download members action 595 popupMenuHandler.addAction(downloadMembersAction); 596 597 // -- download incomplete members action 598 popupMenuHandler.addAction(downloadSelectedIncompleteMembersAction); 599 600 popupMenuHandler.addSeparator(); 601 popupMenuHandler.addAction(editAction).setVisible(false); 602 popupMenuHandler.addAction(duplicateAction).setVisible(false); 603 popupMenuHandler.addAction(deleteRelationsAction).setVisible(false); 604 605 popupMenuHandler.addAction(addSelectionToRelations); 606 } 607 608 /* ---------------------------------------------------------------------------------- */ 609 /* Methods that can be called from plugins */ 610 /* ---------------------------------------------------------------------------------- */ 611 612 /** 613 * Replies the popup menu handler. 614 * @return The popup menu handler 615 */ 616 public PopupMenuHandler getPopupMenuHandler() { 617 return popupMenuHandler; 618 } 619 620 /** 621 * Replies the list of selected relations. Empty list, if there are no selected relations. 622 * @return the list of selected, non-new relations. 623 */ 624 public Collection<Relation> getSelectedRelations() { 625 return model.getSelectedRelations(); 626 } 627 628 /* ---------------------------------------------------------------------------------- */ 629 /* DataSetListener */ 630 /* ---------------------------------------------------------------------------------- */ 631 632 @Override 633 public void nodeMoved(NodeMovedEvent event) {/* irrelevant in this context */} 634 635 @Override 636 public void wayNodesChanged(WayNodesChangedEvent event) {/* irrelevant in this context */} 637 638 @Override 639 public void primitivesAdded(final PrimitivesAddedEvent event) { 640 model.addRelations(event.getPrimitives()); 641 model.updateTitle(); 642 } 643 644 @Override 645 public void primitivesRemoved(final PrimitivesRemovedEvent event) { 646 model.removeRelations(event.getPrimitives()); 647 model.updateTitle(); 648 } 649 650 @Override 651 public void relationMembersChanged(final RelationMembersChangedEvent event) { 652 List<Relation> sel = model.getSelectedRelations(); 653 model.sort(); 654 model.setSelectedRelations(sel); 655 displaylist.repaint(); 656 } 657 658 @Override 659 public void tagsChanged(TagsChangedEvent event) { 660 OsmPrimitive prim = event.getPrimitive(); 661 if (!(prim instanceof Relation)) 662 return; 663 // trigger a sort of the relation list because the display name may have changed 664 // 665 List<Relation> sel = model.getSelectedRelations(); 666 model.sort(); 667 model.setSelectedRelations(sel); 668 displaylist.repaint(); 669 } 670 671 @Override 672 public void dataChanged(DataChangedEvent event) { 673 initFromLayer(Main.main.getEditLayer()); 674 } 675 676 @Override 677 public void otherDatasetChange(AbstractDatasetChangedEvent event) { 678 /* ignore */ 679 } 680}