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