source: josm/trunk/src/org/openstreetmap/josm/gui/dialogs/RelationListDialog.java @ 9264

Last change on this file since 9264 was 9264, checked in by simon04, 3 years ago

see #7089 - Relation list: re-filter after zooming

This allows to use filters such as parent inview to only display relations w/ members in the current view.

  • Property svn:eol-style set to native
File size: 24.7 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.gui.dialogs;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5
6import java.awt.BorderLayout;
7import java.awt.Component;
8import java.awt.event.ActionEvent;
9import java.awt.event.KeyEvent;
10import java.awt.event.MouseEvent;
11import java.beans.PropertyChangeEvent;
12import java.beans.PropertyChangeListener;
13import java.util.ArrayList;
14import java.util.Arrays;
15import java.util.Collection;
16import java.util.Collections;
17import java.util.HashSet;
18import java.util.List;
19import java.util.Set;
20
21import javax.swing.AbstractAction;
22import javax.swing.AbstractListModel;
23import javax.swing.DefaultListSelectionModel;
24import javax.swing.FocusManager;
25import javax.swing.JComponent;
26import javax.swing.JList;
27import javax.swing.JPanel;
28import javax.swing.JPopupMenu;
29import javax.swing.JScrollPane;
30import javax.swing.KeyStroke;
31import javax.swing.ListSelectionModel;
32import javax.swing.event.ListSelectionEvent;
33import javax.swing.event.ListSelectionListener;
34
35import org.openstreetmap.josm.Main;
36import org.openstreetmap.josm.actions.relation.AddSelectionToRelations;
37import org.openstreetmap.josm.actions.relation.DeleteRelationsAction;
38import org.openstreetmap.josm.actions.relation.DownloadMembersAction;
39import org.openstreetmap.josm.actions.relation.DownloadSelectedIncompleteMembersAction;
40import org.openstreetmap.josm.actions.relation.DuplicateRelationAction;
41import org.openstreetmap.josm.actions.relation.EditRelationAction;
42import org.openstreetmap.josm.actions.relation.SelectMembersAction;
43import org.openstreetmap.josm.actions.relation.SelectRelationAction;
44import org.openstreetmap.josm.actions.search.SearchCompiler;
45import org.openstreetmap.josm.data.osm.DataSet;
46import org.openstreetmap.josm.data.osm.OsmPrimitive;
47import org.openstreetmap.josm.data.osm.Relation;
48import org.openstreetmap.josm.data.osm.event.AbstractDatasetChangedEvent;
49import org.openstreetmap.josm.data.osm.event.DataChangedEvent;
50import org.openstreetmap.josm.data.osm.event.DataSetListener;
51import org.openstreetmap.josm.data.osm.event.DatasetEventManager;
52import org.openstreetmap.josm.data.osm.event.DatasetEventManager.FireMode;
53import org.openstreetmap.josm.data.osm.event.NodeMovedEvent;
54import org.openstreetmap.josm.data.osm.event.PrimitivesAddedEvent;
55import org.openstreetmap.josm.data.osm.event.PrimitivesRemovedEvent;
56import org.openstreetmap.josm.data.osm.event.RelationMembersChangedEvent;
57import org.openstreetmap.josm.data.osm.event.TagsChangedEvent;
58import org.openstreetmap.josm.data.osm.event.WayNodesChangedEvent;
59import org.openstreetmap.josm.gui.DefaultNameFormatter;
60import org.openstreetmap.josm.gui.MapView;
61import org.openstreetmap.josm.gui.MapView.LayerChangeListener;
62import org.openstreetmap.josm.gui.NavigatableComponent;
63import org.openstreetmap.josm.gui.OsmPrimitivRenderer;
64import org.openstreetmap.josm.gui.PopupMenuHandler;
65import org.openstreetmap.josm.gui.SideButton;
66import org.openstreetmap.josm.gui.dialogs.relation.RelationEditor;
67import org.openstreetmap.josm.gui.layer.Layer;
68import org.openstreetmap.josm.gui.layer.OsmDataLayer;
69import org.openstreetmap.josm.gui.util.GuiHelper;
70import org.openstreetmap.josm.gui.util.HighlightHelper;
71import org.openstreetmap.josm.gui.widgets.CompileSearchTextDecorator;
72import org.openstreetmap.josm.gui.widgets.DisableShortcutsOnFocusGainedTextField;
73import org.openstreetmap.josm.gui.widgets.JosmTextField;
74import org.openstreetmap.josm.gui.widgets.PopupMenuLauncher;
75import org.openstreetmap.josm.tools.ImageProvider;
76import org.openstreetmap.josm.tools.InputMapUtils;
77import org.openstreetmap.josm.tools.Predicate;
78import org.openstreetmap.josm.tools.Shortcut;
79import org.openstreetmap.josm.tools.Utils;
80
81/**
82 * A dialog showing all known relations, with buttons to add, edit, and
83 * delete them.
84 *
85 * We don't have such dialogs for nodes, segments, and ways, because those
86 * objects are visible on the map and can be selected there. Relations are not.
87 */
88public class RelationListDialog extends ToggleDialog implements DataSetListener, NavigatableComponent.ZoomChangeListener {
89    /** The display list. */
90    private final JList<Relation> displaylist;
91    /** the list model used */
92    private final RelationListModel model;
93
94    private final NewAction newAction;
95
96    /** the popup menu and its handler */
97    private final JPopupMenu popupMenu = new JPopupMenu();
98    private final transient PopupMenuHandler popupMenuHandler = new PopupMenuHandler(popupMenu);
99
100    private final JosmTextField filter;
101
102    // Actions
103    /** the edit action */
104    private final EditRelationAction editAction = new EditRelationAction();
105    /** the delete action */
106    private final DeleteRelationsAction deleteRelationsAction = new DeleteRelationsAction();
107    /** the duplicate action */
108    private final DuplicateRelationAction duplicateAction = new DuplicateRelationAction();
109    private final DownloadMembersAction downloadMembersAction = new DownloadMembersAction();
110    private final DownloadSelectedIncompleteMembersAction downloadSelectedIncompleteMembersAction =
111            new DownloadSelectedIncompleteMembersAction();
112    private final SelectMembersAction selectMembersAction = new SelectMembersAction(false);
113    private final SelectMembersAction addMembersToSelectionAction = new SelectMembersAction(true);
114    private final SelectRelationAction selectRelationAction = new SelectRelationAction(false);
115    private final SelectRelationAction addRelationToSelectionAction = new SelectRelationAction(true);
116    /** add all selected primitives to the given relations */
117    private final AddSelectionToRelations addSelectionToRelations = new AddSelectionToRelations();
118
119    private final transient HighlightHelper highlightHelper = new HighlightHelper();
120    private final boolean highlightEnabled = Main.pref.getBoolean("draw.target-highlight", true);
121
122    /**
123     * Constructs <code>RelationListDialog</code>
124     */
125    public RelationListDialog() {
126        super(tr("Relations"), "relationlist", tr("Open a list of all relations."),
127                Shortcut.registerShortcut("subwindow:relations", tr("Toggle: {0}", tr("Relations")),
128                KeyEvent.VK_R, Shortcut.ALT_SHIFT), 150, true);
129
130        // create the list of relations
131        //
132        DefaultListSelectionModel selectionModel = new DefaultListSelectionModel();
133        model = new RelationListModel(selectionModel);
134        displaylist = new JList<>(model);
135        displaylist.setSelectionModel(selectionModel);
136        displaylist.setCellRenderer(new OsmPrimitivRenderer() {
137            /**
138             * Don't show the default tooltip in the relation list.
139             */
140            @Override
141            protected String getComponentToolTipText(OsmPrimitive value) {
142                return null;
143            }
144        });
145        displaylist.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
146        displaylist.addMouseListener(new MouseEventHandler());
147
148        // the new action
149        //
150        newAction = new NewAction();
151
152        filter = setupFilter();
153
154        displaylist.addListSelectionListener(new ListSelectionListener() {
155            @Override
156            public void valueChanged(ListSelectionEvent e) {
157                updateActionsRelationLists();
158            }
159        });
160
161        // Setup popup menu handler
162        setupPopupMenuHandler();
163
164        JPanel pane = new JPanel(new BorderLayout());
165        pane.add(filter, BorderLayout.NORTH);
166        pane.add(new JScrollPane(displaylist), BorderLayout.CENTER);
167        createLayout(pane, false, Arrays.asList(new SideButton[]{
168                new SideButton(newAction, false),
169                new SideButton(editAction, false),
170                new SideButton(duplicateAction, false),
171                new SideButton(deleteRelationsAction, false),
172                new SideButton(selectRelationAction, false)
173        }));
174
175        InputMapUtils.unassignCtrlShiftUpDown(displaylist, JComponent.WHEN_FOCUSED);
176
177        // Select relation on Enter
178        InputMapUtils.addEnterAction(displaylist, selectRelationAction);
179
180        // Edit relation on Ctrl-Enter
181        displaylist.getActionMap().put("edit", editAction);
182        displaylist.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, KeyEvent.CTRL_MASK), "edit");
183
184        // Do not hide copy action because of default JList override (fix #9815)
185        displaylist.getActionMap().put("copy", Main.main.menu.copy);
186        displaylist.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_C, GuiHelper.getMenuShortcutKeyMaskEx()), "copy");
187
188        updateActionsRelationLists();
189    }
190
191    // inform all actions about list of relations they need
192    private void updateActionsRelationLists() {
193        List<Relation> sel = model.getSelectedRelations();
194        popupMenuHandler.setPrimitives(sel);
195
196        Component focused = FocusManager.getCurrentKeyboardFocusManager().getFocusOwner();
197
198        //update highlights
199        if (highlightEnabled && focused == displaylist && Main.isDisplayingMapView()) {
200            if (highlightHelper.highlightOnly(sel)) {
201                Main.map.mapView.repaint();
202            }
203        }
204    }
205
206    @Override
207    public void showNotify() {
208        MapView.addLayerChangeListener(newAction);
209        MapView.addZoomChangeListener(this);
210        newAction.updateEnabledState();
211        DatasetEventManager.getInstance().addDatasetListener(this, FireMode.IN_EDT);
212        DataSet.addSelectionListener(addSelectionToRelations);
213        dataChanged(null);
214    }
215
216    @Override
217    public void hideNotify() {
218        MapView.removeLayerChangeListener(newAction);
219        MapView.removeZoomChangeListener(this);
220        DatasetEventManager.getInstance().removeDatasetListener(this);
221        DataSet.removeSelectionListener(addSelectionToRelations);
222    }
223
224    private void resetFilter() {
225        filter.setText(null);
226    }
227
228    /**
229     * Initializes the relation list dialog from a layer. If <code>layer</code> is null
230     * or if it isn't an {@link OsmDataLayer} the dialog is reset to an empty dialog.
231     * Otherwise it is initialized with the list of non-deleted and visible relations
232     * in the layer's dataset.
233     *
234     * @param layer the layer. May be null.
235     */
236    protected void initFromLayer(Layer layer) {
237        if (!(layer instanceof OsmDataLayer)) {
238            model.setRelations(null);
239            return;
240        }
241        OsmDataLayer l = (OsmDataLayer) layer;
242        model.setRelations(l.data.getRelations());
243        model.updateTitle();
244        updateActionsRelationLists();
245    }
246
247    /**
248     * @return The selected relation in the list
249     */
250    private Relation getSelected() {
251        if (model.getSize() == 1) {
252            displaylist.setSelectedIndex(0);
253        }
254        return displaylist.getSelectedValue();
255    }
256
257    /**
258     * Selects the relation <code>relation</code> in the list of relations.
259     *
260     * @param relation  the relation
261     */
262    public void selectRelation(Relation relation) {
263        selectRelations(Collections.singleton(relation));
264    }
265
266    /**
267     * Selects the relations in the list of relations.
268     * @param relations  the relations to be selected
269     */
270    public void selectRelations(Collection<Relation> relations) {
271        if (relations == null || relations.isEmpty()) {
272            model.setSelectedRelations(null);
273        } else {
274            model.setSelectedRelations(relations);
275            Integer i = model.getVisibleRelationIndex(relations.iterator().next());
276            if (i != null) {
277                // Not all relations have to be in the list
278                // (for example when the relation list is hidden, it's not updated with new relations)
279                displaylist.scrollRectToVisible(displaylist.getCellBounds(i, i));
280            }
281        }
282    }
283
284    private JosmTextField  setupFilter() {
285        final JosmTextField f = new DisableShortcutsOnFocusGainedTextField();
286        f.setToolTipText(tr("Relation list filter"));
287        final CompileSearchTextDecorator decorator = CompileSearchTextDecorator.decorate(f);
288        f.addPropertyChangeListener("filter", new PropertyChangeListener() {
289            @Override
290            public void propertyChange(PropertyChangeEvent evt) {
291                model.setFilter(decorator.getMatch());
292            }
293        });
294        return f;
295    }
296
297    class MouseEventHandler extends PopupMenuLauncher {
298
299        MouseEventHandler() {
300            super(popupMenu);
301        }
302
303        @Override
304        public void mouseExited(MouseEvent me) {
305            if (highlightEnabled) highlightHelper.clear();
306        }
307
308        protected void setCurrentRelationAsSelection() {
309            Main.main.getCurrentDataSet().setSelected(displaylist.getSelectedValue());
310        }
311
312        protected void editCurrentRelation() {
313            EditRelationAction.launchEditor(getSelected());
314        }
315
316        @Override
317        public void mouseClicked(MouseEvent e) {
318            if (!Main.main.hasEditLayer()) return;
319            if (isDoubleClick(e)) {
320                if (e.isControlDown()) {
321                    editCurrentRelation();
322                } else {
323                    setCurrentRelationAsSelection();
324                }
325            }
326        }
327    }
328
329    /**
330     * The action for creating a new relation
331     *
332     */
333    static class NewAction extends AbstractAction implements LayerChangeListener {
334        NewAction() {
335            putValue(SHORT_DESCRIPTION, tr("Create a new relation"));
336            putValue(NAME, tr("New"));
337            putValue(SMALL_ICON, ImageProvider.get("dialogs", "addrelation"));
338            updateEnabledState();
339        }
340
341        public void run() {
342            RelationEditor.getEditor(Main.main.getEditLayer(), null, null).setVisible(true);
343        }
344
345        @Override
346        public void actionPerformed(ActionEvent e) {
347            run();
348        }
349
350        protected void updateEnabledState() {
351            setEnabled(Main.main != null && Main.main.hasEditLayer());
352        }
353
354        @Override
355        public void activeLayerChange(Layer oldLayer, Layer newLayer) {
356            updateEnabledState();
357        }
358
359        @Override
360        public void layerAdded(Layer newLayer) {
361            updateEnabledState();
362        }
363
364        @Override
365        public void layerRemoved(Layer oldLayer) {
366            updateEnabledState();
367        }
368    }
369
370    /**
371     * The list model for the list of relations displayed in the relation list dialog.
372     *
373     */
374    private class RelationListModel extends AbstractListModel<Relation> {
375        private final transient List<Relation> relations = new ArrayList<>();
376        private transient List<Relation> filteredRelations;
377        private final DefaultListSelectionModel selectionModel;
378        private transient SearchCompiler.Match filter;
379
380        RelationListModel(DefaultListSelectionModel selectionModel) {
381            this.selectionModel = selectionModel;
382        }
383
384        public void sort() {
385            Collections.sort(
386                    relations,
387                    DefaultNameFormatter.getInstance().getRelationComparator()
388                    );
389        }
390
391        private boolean isValid(Relation r) {
392            return !r.isDeleted() && r.isVisible() && !r.isIncomplete();
393        }
394
395        public void setRelations(Collection<Relation> relations) {
396            List<Relation> sel =  getSelectedRelations();
397            this.relations.clear();
398            this.filteredRelations = null;
399            if (relations == null) {
400                selectionModel.clearSelection();
401                fireContentsChanged(this, 0, getSize());
402                return;
403            }
404            for (Relation r: relations) {
405                if (isValid(r)) {
406                    this.relations.add(r);
407                }
408            }
409            sort();
410            updateFilteredRelations();
411            fireIntervalAdded(this, 0, getSize());
412            setSelectedRelations(sel);
413        }
414
415        /**
416         * Add all relations in <code>addedPrimitives</code> to the model for the
417         * relation list dialog
418         *
419         * @param addedPrimitives the collection of added primitives. May include nodes,
420         * ways, and relations.
421         */
422        public void addRelations(Collection<? extends OsmPrimitive> addedPrimitives) {
423            boolean added = false;
424            for (OsmPrimitive p: addedPrimitives) {
425                if (!(p instanceof Relation)) {
426                    continue;
427                }
428
429                Relation r = (Relation) p;
430                if (relations.contains(r)) {
431                    continue;
432                }
433                if (isValid(r)) {
434                    relations.add(r);
435                    added = true;
436                }
437            }
438            if (added) {
439                List<Relation> sel = getSelectedRelations();
440                sort();
441                updateFilteredRelations();
442                fireIntervalAdded(this, 0, getSize());
443                setSelectedRelations(sel);
444            }
445        }
446
447        /**
448         * Removes all relations in <code>removedPrimitives</code> from the model
449         *
450         * @param removedPrimitives the removed primitives. May include nodes, ways,
451         *   and relations
452         */
453        public void removeRelations(Collection<? extends OsmPrimitive> removedPrimitives) {
454            if (removedPrimitives == null) return;
455            // extract the removed relations
456            //
457            Set<Relation> removedRelations = new HashSet<>();
458            for (OsmPrimitive p: removedPrimitives) {
459                if (!(p instanceof Relation)) {
460                    continue;
461                }
462                removedRelations.add((Relation) p);
463            }
464            if (removedRelations.isEmpty())
465                return;
466            int size = relations.size();
467            relations.removeAll(removedRelations);
468            if (filteredRelations != null) {
469                filteredRelations.removeAll(removedRelations);
470            }
471            if (size != relations.size()) {
472                List<Relation> sel = getSelectedRelations();
473                sort();
474                fireContentsChanged(this, 0, getSize());
475                setSelectedRelations(sel);
476            }
477        }
478
479        private void updateFilteredRelations() {
480            if (filter != null) {
481                filteredRelations = new ArrayList<>(Utils.filter(relations, new Predicate<Relation>() {
482                    @Override
483                    public boolean evaluate(Relation r) {
484                        return filter.match(r);
485                    }
486                }));
487            } else if (filteredRelations != null) {
488                filteredRelations = null;
489            }
490        }
491
492        public void setFilter(final SearchCompiler.Match filter) {
493            this.filter = filter;
494            updateFilteredRelations();
495            List<Relation> sel = getSelectedRelations();
496            fireContentsChanged(this, 0, getSize());
497            setSelectedRelations(sel);
498            updateTitle();
499        }
500
501        private List<Relation> getVisibleRelations() {
502            return filteredRelations == null ? relations : filteredRelations;
503        }
504
505        private Relation getVisibleRelation(int index) {
506            if (index < 0 || index >= getVisibleRelations().size()) return null;
507            return getVisibleRelations().get(index);
508        }
509
510        @Override
511        public Relation getElementAt(int index) {
512            return getVisibleRelation(index);
513        }
514
515        @Override
516        public int getSize() {
517            return getVisibleRelations().size();
518        }
519
520        /**
521         * Replies the list of selected relations. Empty list,
522         * if there are no selected relations.
523         *
524         * @return the list of selected, non-new relations.
525         */
526        public List<Relation> getSelectedRelations() {
527            List<Relation> ret = new ArrayList<>();
528            for (int i = 0; i < getSize(); i++) {
529                if (!selectionModel.isSelectedIndex(i)) {
530                    continue;
531                }
532                ret.add(getVisibleRelation(i));
533            }
534            return ret;
535        }
536
537        /**
538         * Sets the selected relations.
539         *
540         * @param sel the list of selected relations
541         */
542        public void setSelectedRelations(Collection<Relation> sel) {
543            selectionModel.clearSelection();
544            if (sel == null || sel.isEmpty())
545                return;
546            if (!getVisibleRelations().containsAll(sel)) {
547                resetFilter();
548            }
549            for (Relation r: sel) {
550                Integer i = getVisibleRelationIndex(r);
551                if (i != null) {
552                    selectionModel.addSelectionInterval(i, i);
553                }
554            }
555        }
556
557        private Integer getVisibleRelationIndex(Relation rel) {
558            int i = getVisibleRelations().indexOf(rel);
559            if (i < 0)
560                return null;
561            return i;
562        }
563
564        public void updateTitle() {
565            if (!relations.isEmpty() && relations.size() != getSize()) {
566                RelationListDialog.this.setTitle(tr("Relations: {0}/{1}", getSize(), relations.size()));
567            } else if (getSize() > 0) {
568                RelationListDialog.this.setTitle(tr("Relations: {0}", getSize()));
569            } else {
570                RelationListDialog.this.setTitle(tr("Relations"));
571            }
572        }
573    }
574
575    private void setupPopupMenuHandler() {
576
577        // -- select action
578        popupMenuHandler.addAction(selectRelationAction);
579        popupMenuHandler.addAction(addRelationToSelectionAction);
580
581        // -- select members action
582        popupMenuHandler.addAction(selectMembersAction);
583        popupMenuHandler.addAction(addMembersToSelectionAction);
584
585        popupMenuHandler.addSeparator();
586        // -- download members action
587        popupMenuHandler.addAction(downloadMembersAction);
588
589        // -- download incomplete members action
590        popupMenuHandler.addAction(downloadSelectedIncompleteMembersAction);
591
592        popupMenuHandler.addSeparator();
593        popupMenuHandler.addAction(editAction).setVisible(false);
594        popupMenuHandler.addAction(duplicateAction).setVisible(false);
595        popupMenuHandler.addAction(deleteRelationsAction).setVisible(false);
596
597        popupMenuHandler.addAction(addSelectionToRelations);
598    }
599
600    /* ---------------------------------------------------------------------------------- */
601    /* Methods that can be called from plugins                                            */
602    /* ---------------------------------------------------------------------------------- */
603
604    /**
605     * Replies the popup menu handler.
606     * @return The popup menu handler
607     */
608    public PopupMenuHandler getPopupMenuHandler() {
609        return popupMenuHandler;
610    }
611
612    /**
613     * Replies the list of selected relations. Empty list, if there are no selected relations.
614     * @return the list of selected, non-new relations.
615     */
616    public Collection<Relation> getSelectedRelations() {
617        return model.getSelectedRelations();
618    }
619
620    /* ---------------------------------------------------------------------------------- */
621    /* DataSetListener                                                                    */
622    /* ---------------------------------------------------------------------------------- */
623
624    @Override
625    public void nodeMoved(NodeMovedEvent event) {
626        /* irrelevant in this context */
627    }
628
629    @Override
630    public void wayNodesChanged(WayNodesChangedEvent event) {
631        /* irrelevant in this context */
632    }
633
634    @Override
635    public void primitivesAdded(final PrimitivesAddedEvent event) {
636        model.addRelations(event.getPrimitives());
637        model.updateTitle();
638    }
639
640    @Override
641    public void primitivesRemoved(final PrimitivesRemovedEvent event) {
642        model.removeRelations(event.getPrimitives());
643        model.updateTitle();
644    }
645
646    @Override
647    public void relationMembersChanged(final RelationMembersChangedEvent event) {
648        List<Relation> sel = model.getSelectedRelations();
649        model.sort();
650        model.setSelectedRelations(sel);
651        displaylist.repaint();
652    }
653
654    @Override
655    public void tagsChanged(TagsChangedEvent event) {
656        OsmPrimitive prim = event.getPrimitive();
657        if (!(prim instanceof Relation))
658            return;
659        // trigger a sort of the relation list because the display name may have changed
660        //
661        List<Relation> sel = model.getSelectedRelations();
662        model.sort();
663        model.setSelectedRelations(sel);
664        displaylist.repaint();
665    }
666
667    @Override
668    public void dataChanged(DataChangedEvent event) {
669        initFromLayer(Main.main.getEditLayer());
670    }
671
672    @Override
673    public void otherDatasetChange(AbstractDatasetChangedEvent event) {
674        /* ignore */
675    }
676
677    @Override
678    public void zoomChanged() {
679        // re-filter relations
680        if (model.filter != null) {
681            model.setFilter(model.filter);
682        }
683    }
684}
Note: See TracBrowser for help on using the repository browser.