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