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