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.GuiHelper; 071import org.openstreetmap.josm.gui.util.HighlightHelper; 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 { 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 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 = 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 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) { // Not all relations have to be in the list (for example when the relation list is hidden, it's not updated with new relations) 274 displaylist.scrollRectToVisible(displaylist.getCellBounds(i, i)); 275 } 276 } 277 } 278 279 private JosmTextField setupFilter() { 280 final JosmTextField f = new DisableShortcutsOnFocusGainedTextField(); 281 f.setToolTipText(tr("Relation list filter")); 282 f.getDocument().addDocumentListener(new DocumentListener() { 283 284 private void setFilter() { 285 try { 286 f.setBackground(UIManager.getColor("TextField.background")); 287 f.setToolTipText(tr("Relation list filter")); 288 model.setFilter(SearchCompiler.compile(filter.getText(), false, false)); 289 } catch (SearchCompiler.ParseError ex) { 290 f.setBackground(new Color(255, 224, 224)); 291 f.setToolTipText(ex.getMessage()); 292 model.setFilter(new SearchCompiler.Always()); 293 } 294 } 295 296 @Override 297 public void insertUpdate(DocumentEvent e) { 298 setFilter(); 299 } 300 301 @Override 302 public void removeUpdate(DocumentEvent e) { 303 setFilter(); 304 } 305 306 @Override 307 public void changedUpdate(DocumentEvent e) { 308 setFilter(); 309 } 310 }); 311 return f; 312 } 313 314 class MouseEventHandler extends PopupMenuLauncher { 315 316 public MouseEventHandler() { 317 super(popupMenu); 318 } 319 320 @Override 321 public void mouseExited(MouseEvent me) { 322 if (highlightEnabled) highlightHelper.clear(); 323 } 324 325 protected void setCurrentRelationAsSelection() { 326 Main.main.getCurrentDataSet().setSelected(displaylist.getSelectedValue()); 327 } 328 329 protected void editCurrentRelation() { 330 EditRelationAction.launchEditor(getSelected()); 331 } 332 333 @Override 334 public void mouseClicked(MouseEvent e) { 335 if (!Main.main.hasEditLayer()) return; 336 if (isDoubleClick(e)) { 337 if (e.isControlDown()) { 338 editCurrentRelation(); 339 } else { 340 setCurrentRelationAsSelection(); 341 } 342 } 343 } 344 } 345 346 /** 347 * The action for creating a new relation 348 * 349 */ 350 static class NewAction extends AbstractAction implements LayerChangeListener{ 351 public NewAction() { 352 putValue(SHORT_DESCRIPTION,tr("Create a new relation")); 353 putValue(NAME, tr("New")); 354 putValue(SMALL_ICON, ImageProvider.get("dialogs", "addrelation")); 355 updateEnabledState(); 356 } 357 358 public void run() { 359 RelationEditor.getEditor(Main.main.getEditLayer(),null, null).setVisible(true); 360 } 361 362 @Override 363 public void actionPerformed(ActionEvent e) { 364 run(); 365 } 366 367 protected void updateEnabledState() { 368 setEnabled(Main.main != null && Main.main.hasEditLayer()); 369 } 370 371 @Override 372 public void activeLayerChange(Layer oldLayer, Layer newLayer) { 373 updateEnabledState(); 374 } 375 376 @Override 377 public void layerAdded(Layer newLayer) { 378 updateEnabledState(); 379 } 380 381 @Override 382 public void layerRemoved(Layer oldLayer) { 383 updateEnabledState(); 384 } 385 } 386 387 /** 388 * The list model for the list of relations displayed in the relation list dialog. 389 * 390 */ 391 private class RelationListModel extends AbstractListModel<Relation> { 392 private final List<Relation> relations = new ArrayList<>(); 393 private List<Relation> filteredRelations; 394 private DefaultListSelectionModel selectionModel; 395 private SearchCompiler.Match filter; 396 397 public RelationListModel(DefaultListSelectionModel selectionModel) { 398 this.selectionModel = selectionModel; 399 } 400 401 public void sort() { 402 Collections.sort( 403 relations, 404 DefaultNameFormatter.getInstance().getRelationComparator() 405 ); 406 } 407 408 private boolean isValid(Relation r) { 409 return !r.isDeleted() && r.isVisible() && !r.isIncomplete(); 410 } 411 412 public void setRelations(Collection<Relation> relations) { 413 List<Relation> sel = getSelectedRelations(); 414 this.relations.clear(); 415 this.filteredRelations = null; 416 if (relations == null) { 417 selectionModel.clearSelection(); 418 fireContentsChanged(this,0,getSize()); 419 return; 420 } 421 for (Relation r: relations) { 422 if (isValid(r)) { 423 this.relations.add(r); 424 } 425 } 426 sort(); 427 updateFilteredRelations(); 428 fireIntervalAdded(this, 0, getSize()); 429 setSelectedRelations(sel); 430 } 431 432 /** 433 * Add all relations in <code>addedPrimitives</code> to the model for the 434 * relation list dialog 435 * 436 * @param addedPrimitives the collection of added primitives. May include nodes, 437 * ways, and relations. 438 */ 439 public void addRelations(Collection<? extends OsmPrimitive> addedPrimitives) { 440 boolean added = false; 441 for (OsmPrimitive p: addedPrimitives) { 442 if (! (p instanceof Relation)) { 443 continue; 444 } 445 446 Relation r = (Relation)p; 447 if (relations.contains(r)) { 448 continue; 449 } 450 if (isValid(r)) { 451 relations.add(r); 452 added = true; 453 } 454 } 455 if (added) { 456 List<Relation> sel = getSelectedRelations(); 457 sort(); 458 updateFilteredRelations(); 459 fireIntervalAdded(this, 0, getSize()); 460 setSelectedRelations(sel); 461 } 462 } 463 464 /** 465 * Removes all relations in <code>removedPrimitives</code> from the model 466 * 467 * @param removedPrimitives the removed primitives. May include nodes, ways, 468 * and relations 469 */ 470 public void removeRelations(Collection<? extends OsmPrimitive> removedPrimitives) { 471 if (removedPrimitives == null) return; 472 // extract the removed relations 473 // 474 Set<Relation> removedRelations = new HashSet<>(); 475 for (OsmPrimitive p: removedPrimitives) { 476 if (! (p instanceof Relation)) { 477 continue; 478 } 479 removedRelations.add((Relation)p); 480 } 481 if (removedRelations.isEmpty()) 482 return; 483 int size = relations.size(); 484 relations.removeAll(removedRelations); 485 if (filteredRelations != null) { 486 filteredRelations.removeAll(removedRelations); 487 } 488 if (size != relations.size()) { 489 List<Relation> sel = getSelectedRelations(); 490 sort(); 491 fireContentsChanged(this, 0, getSize()); 492 setSelectedRelations(sel); 493 } 494 } 495 496 private void updateFilteredRelations() { 497 if (filter != null) { 498 filteredRelations = new ArrayList<>(Utils.filter(relations, new Predicate<Relation>() { 499 @Override 500 public boolean evaluate(Relation r) { 501 return filter.match(r); 502 } 503 })); 504 } else if (filteredRelations != null) { 505 filteredRelations = null; 506 } 507 } 508 509 public void setFilter(final SearchCompiler.Match filter) { 510 this.filter = filter; 511 updateFilteredRelations(); 512 List<Relation> sel = getSelectedRelations(); 513 fireContentsChanged(this, 0, getSize()); 514 setSelectedRelations(sel); 515 updateTitle(); 516 } 517 518 private List<Relation> getVisibleRelations() { 519 return filteredRelations == null ? relations : filteredRelations; 520 } 521 522 private Relation getVisibleRelation(int index) { 523 if (index < 0 || index >= getVisibleRelations().size()) return null; 524 return getVisibleRelations().get(index); 525 } 526 527 @Override 528 public Relation getElementAt(int index) { 529 return getVisibleRelation(index); 530 } 531 532 @Override 533 public int getSize() { 534 return getVisibleRelations().size(); 535 } 536 537 /** 538 * Replies the list of selected relations. Empty list, 539 * if there are no selected relations. 540 * 541 * @return the list of selected, non-new relations. 542 */ 543 public List<Relation> getSelectedRelations() { 544 List<Relation> ret = new ArrayList<>(); 545 for (int i=0; i<getSize();i++) { 546 if (!selectionModel.isSelectedIndex(i)) { 547 continue; 548 } 549 ret.add(getVisibleRelation(i)); 550 } 551 return ret; 552 } 553 554 /** 555 * Sets the selected relations. 556 * 557 * @param sel the list of selected relations 558 */ 559 public void setSelectedRelations(Collection<Relation> sel) { 560 selectionModel.clearSelection(); 561 if (sel == null || sel.isEmpty()) 562 return; 563 if (!getVisibleRelations().containsAll(sel)) { 564 resetFilter(); 565 } 566 for (Relation r: sel) { 567 Integer i = getVisibleRelationIndex(r); 568 if (i != null) { 569 selectionModel.addSelectionInterval(i,i); 570 } 571 } 572 } 573 574 private Integer getVisibleRelationIndex(Relation rel) { 575 int i = getVisibleRelations().indexOf(rel); 576 if (i<0) 577 return null; 578 return i; 579 } 580 581 public void updateTitle() { 582 if (relations.size() > 0 && relations.size() != getSize()) { 583 RelationListDialog.this.setTitle(tr("Relations: {0}/{1}", getSize(), relations.size())); 584 } else if (getSize() > 0) { 585 RelationListDialog.this.setTitle(tr("Relations: {0}", getSize())); 586 } else { 587 RelationListDialog.this.setTitle(tr("Relations")); 588 } 589 } 590 } 591 592 private final void setupPopupMenuHandler() { 593 594 // -- select action 595 popupMenuHandler.addAction(selectRelationAction); 596 popupMenuHandler.addAction(addRelationToSelectionAction); 597 598 // -- select members action 599 popupMenuHandler.addAction(selectMembersAction); 600 popupMenuHandler.addAction(addMembersToSelectionAction); 601 602 popupMenuHandler.addSeparator(); 603 // -- download members action 604 popupMenuHandler.addAction(downloadMembersAction); 605 606 // -- download incomplete members action 607 popupMenuHandler.addAction(downloadSelectedIncompleteMembersAction); 608 609 popupMenuHandler.addSeparator(); 610 popupMenuHandler.addAction(editAction).setVisible(false); 611 popupMenuHandler.addAction(duplicateAction).setVisible(false); 612 popupMenuHandler.addAction(deleteRelationsAction).setVisible(false); 613 614 popupMenuHandler.addAction(addSelectionToRelations); 615 } 616 617 /* ---------------------------------------------------------------------------------- */ 618 /* Methods that can be called from plugins */ 619 /* ---------------------------------------------------------------------------------- */ 620 621 /** 622 * Replies the popup menu handler. 623 * @return The popup menu handler 624 */ 625 public PopupMenuHandler getPopupMenuHandler() { 626 return popupMenuHandler; 627 } 628 629 /** 630 * Replies the list of selected relations. Empty list, if there are no selected relations. 631 * @return the list of selected, non-new relations. 632 */ 633 public Collection<Relation> getSelectedRelations() { 634 return model.getSelectedRelations(); 635 } 636 637 /* ---------------------------------------------------------------------------------- */ 638 /* DataSetListener */ 639 /* ---------------------------------------------------------------------------------- */ 640 641 @Override 642 public void nodeMoved(NodeMovedEvent event) {/* irrelevant in this context */} 643 644 @Override 645 public void wayNodesChanged(WayNodesChangedEvent event) {/* irrelevant in this context */} 646 647 @Override 648 public void primitivesAdded(final PrimitivesAddedEvent event) { 649 model.addRelations(event.getPrimitives()); 650 model.updateTitle(); 651 } 652 653 @Override 654 public void primitivesRemoved(final PrimitivesRemovedEvent event) { 655 model.removeRelations(event.getPrimitives()); 656 model.updateTitle(); 657 } 658 659 @Override 660 public void relationMembersChanged(final RelationMembersChangedEvent event) { 661 List<Relation> sel = model.getSelectedRelations(); 662 model.sort(); 663 model.setSelectedRelations(sel); 664 displaylist.repaint(); 665 } 666 667 @Override 668 public void tagsChanged(TagsChangedEvent event) { 669 OsmPrimitive prim = event.getPrimitive(); 670 if (!(prim instanceof Relation)) 671 return; 672 // trigger a sort of the relation list because the display name may have changed 673 // 674 List<Relation> sel = model.getSelectedRelations(); 675 model.sort(); 676 model.setSelectedRelations(sel); 677 displaylist.repaint(); 678 } 679 680 @Override 681 public void dataChanged(DataChangedEvent event) { 682 initFromLayer(Main.main.getEditLayer()); 683 } 684 685 @Override 686 public void otherDatasetChange(AbstractDatasetChangedEvent event) { 687 /* ignore */ 688 } 689}