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_CONSOLIDATED);
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}