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