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