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}