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

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

see #11916 - Refactoring of SearchAction/SearchCompiler

  • Property svn:eol-style set to native
File size: 25.0 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.Color;
8import java.awt.Component;
9import java.awt.event.ActionEvent;
10import java.awt.event.KeyEvent;
11import java.awt.event.MouseEvent;
12import java.util.ArrayList;
13import java.util.Arrays;
14import java.util.Collection;
15import java.util.Collections;
16import java.util.HashSet;
17import java.util.List;
18import java.util.Set;
19
20import javax.swing.AbstractAction;
21import javax.swing.AbstractListModel;
22import javax.swing.DefaultListSelectionModel;
23import javax.swing.FocusManager;
24import javax.swing.JComponent;
25import javax.swing.JList;
26import javax.swing.JPanel;
27import javax.swing.JPopupMenu;
28import javax.swing.JScrollPane;
29import javax.swing.KeyStroke;
30import javax.swing.ListSelectionModel;
31import javax.swing.UIManager;
32import javax.swing.event.DocumentEvent;
33import javax.swing.event.DocumentListener;
34import javax.swing.event.ListSelectionEvent;
35import javax.swing.event.ListSelectionListener;
36
37import org.openstreetmap.josm.Main;
38import org.openstreetmap.josm.actions.relation.AddSelectionToRelations;
39import org.openstreetmap.josm.actions.relation.DeleteRelationsAction;
40import org.openstreetmap.josm.actions.relation.DownloadMembersAction;
41import org.openstreetmap.josm.actions.relation.DownloadSelectedIncompleteMembersAction;
42import org.openstreetmap.josm.actions.relation.DuplicateRelationAction;
43import org.openstreetmap.josm.actions.relation.EditRelationAction;
44import org.openstreetmap.josm.actions.relation.SelectMembersAction;
45import org.openstreetmap.josm.actions.relation.SelectRelationAction;
46import org.openstreetmap.josm.actions.search.SearchCompiler;
47import org.openstreetmap.josm.data.osm.DataSet;
48import org.openstreetmap.josm.data.osm.OsmPrimitive;
49import org.openstreetmap.josm.data.osm.Relation;
50import org.openstreetmap.josm.data.osm.event.AbstractDatasetChangedEvent;
51import org.openstreetmap.josm.data.osm.event.DataChangedEvent;
52import org.openstreetmap.josm.data.osm.event.DataSetListener;
53import org.openstreetmap.josm.data.osm.event.DatasetEventManager;
54import org.openstreetmap.josm.data.osm.event.DatasetEventManager.FireMode;
55import org.openstreetmap.josm.data.osm.event.NodeMovedEvent;
56import org.openstreetmap.josm.data.osm.event.PrimitivesAddedEvent;
57import org.openstreetmap.josm.data.osm.event.PrimitivesRemovedEvent;
58import org.openstreetmap.josm.data.osm.event.RelationMembersChangedEvent;
59import org.openstreetmap.josm.data.osm.event.TagsChangedEvent;
60import org.openstreetmap.josm.data.osm.event.WayNodesChangedEvent;
61import org.openstreetmap.josm.gui.DefaultNameFormatter;
62import org.openstreetmap.josm.gui.MapView;
63import org.openstreetmap.josm.gui.MapView.LayerChangeListener;
64import org.openstreetmap.josm.gui.OsmPrimitivRenderer;
65import org.openstreetmap.josm.gui.PopupMenuHandler;
66import org.openstreetmap.josm.gui.SideButton;
67import org.openstreetmap.josm.gui.dialogs.relation.RelationEditor;
68import org.openstreetmap.josm.gui.layer.Layer;
69import org.openstreetmap.josm.gui.layer.OsmDataLayer;
70import org.openstreetmap.josm.gui.util.GuiHelper;
71import org.openstreetmap.josm.gui.util.HighlightHelper;
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 {
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 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        newAction.updateEnabledState();
210        DatasetEventManager.getInstance().addDatasetListener(this, FireMode.IN_EDT);
211        DataSet.addSelectionListener(addSelectionToRelations);
212        dataChanged(null);
213    }
214
215    @Override
216    public void hideNotify() {
217        MapView.removeLayerChangeListener(newAction);
218        DatasetEventManager.getInstance().removeDatasetListener(this);
219        DataSet.removeSelectionListener(addSelectionToRelations);
220    }
221
222    private void resetFilter() {
223        filter.setText(null);
224    }
225
226    /**
227     * Initializes the relation list dialog from a layer. If <code>layer</code> is null
228     * or if it isn't an {@link OsmDataLayer} the dialog is reset to an empty dialog.
229     * Otherwise it is initialized with the list of non-deleted and visible relations
230     * in the layer's dataset.
231     *
232     * @param layer the layer. May be null.
233     */
234    protected void initFromLayer(Layer layer) {
235        if (!(layer instanceof OsmDataLayer)) {
236            model.setRelations(null);
237            return;
238        }
239        OsmDataLayer l = (OsmDataLayer) layer;
240        model.setRelations(l.data.getRelations());
241        model.updateTitle();
242        updateActionsRelationLists();
243    }
244
245    /**
246     * @return The selected relation in the list
247     */
248    private Relation getSelected() {
249        if (model.getSize() == 1) {
250            displaylist.setSelectedIndex(0);
251        }
252        return displaylist.getSelectedValue();
253    }
254
255    /**
256     * Selects the relation <code>relation</code> in the list of relations.
257     *
258     * @param relation  the relation
259     */
260    public void selectRelation(Relation relation) {
261        selectRelations(Collections.singleton(relation));
262    }
263
264    /**
265     * Selects the relations in the list of relations.
266     * @param relations  the relations to be selected
267     */
268    public void selectRelations(Collection<Relation> relations) {
269        if (relations == null || relations.isEmpty()) {
270            model.setSelectedRelations(null);
271        } else {
272            model.setSelectedRelations(relations);
273            Integer i = model.getVisibleRelationIndex(relations.iterator().next());
274            if (i != null) {
275                // Not all relations have to be in the list
276                // (for example when the relation list is hidden, it's not updated with new relations)
277                displaylist.scrollRectToVisible(displaylist.getCellBounds(i, i));
278            }
279        }
280    }
281
282    private JosmTextField  setupFilter() {
283        final JosmTextField f = new DisableShortcutsOnFocusGainedTextField();
284        f.setToolTipText(tr("Relation list filter"));
285        f.getDocument().addDocumentListener(new DocumentListener() {
286
287            private void setFilter() {
288                try {
289                    f.setBackground(UIManager.getColor("TextField.background"));
290                    f.setToolTipText(tr("Relation list filter"));
291                    model.setFilter(SearchCompiler.compile(filter.getText()));
292                } catch (SearchCompiler.ParseError ex) {
293                    f.setBackground(new Color(255, 224, 224));
294                    f.setToolTipText(ex.getMessage());
295                    model.setFilter(new SearchCompiler.Always());
296                }
297            }
298
299            @Override
300            public void insertUpdate(DocumentEvent e) {
301                setFilter();
302            }
303
304            @Override
305            public void removeUpdate(DocumentEvent e) {
306                setFilter();
307            }
308
309            @Override
310            public void changedUpdate(DocumentEvent e) {
311                setFilter();
312            }
313        });
314        return f;
315    }
316
317    class MouseEventHandler extends PopupMenuLauncher {
318
319        public MouseEventHandler() {
320            super(popupMenu);
321        }
322
323        @Override
324        public void mouseExited(MouseEvent me) {
325            if (highlightEnabled) highlightHelper.clear();
326        }
327
328        protected void setCurrentRelationAsSelection() {
329            Main.main.getCurrentDataSet().setSelected(displaylist.getSelectedValue());
330        }
331
332        protected void editCurrentRelation() {
333            EditRelationAction.launchEditor(getSelected());
334        }
335
336        @Override
337        public void mouseClicked(MouseEvent e) {
338            if (!Main.main.hasEditLayer()) return;
339            if (isDoubleClick(e)) {
340                if (e.isControlDown()) {
341                    editCurrentRelation();
342                } else {
343                    setCurrentRelationAsSelection();
344                }
345            }
346        }
347    }
348
349    /**
350     * The action for creating a new relation
351     *
352     */
353    static class NewAction extends AbstractAction implements LayerChangeListener{
354        public NewAction() {
355            putValue(SHORT_DESCRIPTION, tr("Create a new relation"));
356            putValue(NAME, tr("New"));
357            putValue(SMALL_ICON, ImageProvider.get("dialogs", "addrelation"));
358            updateEnabledState();
359        }
360
361        public void run() {
362            RelationEditor.getEditor(Main.main.getEditLayer(), null, null).setVisible(true);
363        }
364
365        @Override
366        public void actionPerformed(ActionEvent e) {
367            run();
368        }
369
370        protected void updateEnabledState() {
371            setEnabled(Main.main != null && Main.main.hasEditLayer());
372        }
373
374        @Override
375        public void activeLayerChange(Layer oldLayer, Layer newLayer) {
376            updateEnabledState();
377        }
378
379        @Override
380        public void layerAdded(Layer newLayer) {
381            updateEnabledState();
382        }
383
384        @Override
385        public void layerRemoved(Layer oldLayer) {
386            updateEnabledState();
387        }
388    }
389
390    /**
391     * The list model for the list of relations displayed in the relation list dialog.
392     *
393     */
394    private class RelationListModel extends AbstractListModel<Relation> {
395        private final transient List<Relation> relations = new ArrayList<>();
396        private transient List<Relation> filteredRelations;
397        private DefaultListSelectionModel selectionModel;
398        private transient SearchCompiler.Match filter;
399
400        public RelationListModel(DefaultListSelectionModel selectionModel) {
401            this.selectionModel = selectionModel;
402        }
403
404        public void sort() {
405            Collections.sort(
406                    relations,
407                    DefaultNameFormatter.getInstance().getRelationComparator()
408                    );
409        }
410
411        private boolean isValid(Relation r) {
412            return !r.isDeleted() && r.isVisible() && !r.isIncomplete();
413        }
414
415        public void setRelations(Collection<Relation> relations) {
416            List<Relation> sel =  getSelectedRelations();
417            this.relations.clear();
418            this.filteredRelations = null;
419            if (relations == null) {
420                selectionModel.clearSelection();
421                fireContentsChanged(this, 0, getSize());
422                return;
423            }
424            for (Relation r: relations) {
425                if (isValid(r)) {
426                    this.relations.add(r);
427                }
428            }
429            sort();
430            updateFilteredRelations();
431            fireIntervalAdded(this, 0, getSize());
432            setSelectedRelations(sel);
433        }
434
435        /**
436         * Add all relations in <code>addedPrimitives</code> to the model for the
437         * relation list dialog
438         *
439         * @param addedPrimitives the collection of added primitives. May include nodes,
440         * ways, and relations.
441         */
442        public void addRelations(Collection<? extends OsmPrimitive> addedPrimitives) {
443            boolean added = false;
444            for (OsmPrimitive p: addedPrimitives) {
445                if (!(p instanceof Relation)) {
446                    continue;
447                }
448
449                Relation r = (Relation) p;
450                if (relations.contains(r)) {
451                    continue;
452                }
453                if (isValid(r)) {
454                    relations.add(r);
455                    added = true;
456                }
457            }
458            if (added) {
459                List<Relation> sel = getSelectedRelations();
460                sort();
461                updateFilteredRelations();
462                fireIntervalAdded(this, 0, getSize());
463                setSelectedRelations(sel);
464            }
465        }
466
467        /**
468         * Removes all relations in <code>removedPrimitives</code> from the model
469         *
470         * @param removedPrimitives the removed primitives. May include nodes, ways,
471         *   and relations
472         */
473        public void removeRelations(Collection<? extends OsmPrimitive> removedPrimitives) {
474            if (removedPrimitives == null) return;
475            // extract the removed relations
476            //
477            Set<Relation> removedRelations = new HashSet<>();
478            for (OsmPrimitive p: removedPrimitives) {
479                if (!(p instanceof Relation)) {
480                    continue;
481                }
482                removedRelations.add((Relation) p);
483            }
484            if (removedRelations.isEmpty())
485                return;
486            int size = relations.size();
487            relations.removeAll(removedRelations);
488            if (filteredRelations != null) {
489                filteredRelations.removeAll(removedRelations);
490            }
491            if (size != relations.size()) {
492                List<Relation> sel = getSelectedRelations();
493                sort();
494                fireContentsChanged(this, 0, getSize());
495                setSelectedRelations(sel);
496            }
497        }
498
499        private void updateFilteredRelations() {
500            if (filter != null) {
501                filteredRelations = new ArrayList<>(Utils.filter(relations, new Predicate<Relation>() {
502                    @Override
503                    public boolean evaluate(Relation r) {
504                        return filter.match(r);
505                    }
506                }));
507            } else if (filteredRelations != null) {
508                filteredRelations = null;
509            }
510        }
511
512        public void setFilter(final SearchCompiler.Match filter) {
513            this.filter = filter;
514            updateFilteredRelations();
515            List<Relation> sel = getSelectedRelations();
516            fireContentsChanged(this, 0, getSize());
517            setSelectedRelations(sel);
518            updateTitle();
519        }
520
521        private List<Relation> getVisibleRelations() {
522            return filteredRelations == null ? relations : filteredRelations;
523        }
524
525        private Relation getVisibleRelation(int index) {
526            if (index < 0 || index >= getVisibleRelations().size()) return null;
527            return getVisibleRelations().get(index);
528        }
529
530        @Override
531        public Relation getElementAt(int index) {
532            return getVisibleRelation(index);
533        }
534
535        @Override
536        public int getSize() {
537            return getVisibleRelations().size();
538        }
539
540        /**
541         * Replies the list of selected relations. Empty list,
542         * if there are no selected relations.
543         *
544         * @return the list of selected, non-new relations.
545         */
546        public List<Relation> getSelectedRelations() {
547            List<Relation> ret = new ArrayList<>();
548            for (int i = 0; i < getSize(); i++) {
549                if (!selectionModel.isSelectedIndex(i)) {
550                    continue;
551                }
552                ret.add(getVisibleRelation(i));
553            }
554            return ret;
555        }
556
557        /**
558         * Sets the selected relations.
559         *
560         * @param sel the list of selected relations
561         */
562        public void setSelectedRelations(Collection<Relation> sel) {
563            selectionModel.clearSelection();
564            if (sel == null || sel.isEmpty())
565                return;
566            if (!getVisibleRelations().containsAll(sel)) {
567                resetFilter();
568            }
569            for (Relation r: sel) {
570                Integer i = getVisibleRelationIndex(r);
571                if (i != null) {
572                    selectionModel.addSelectionInterval(i, i);
573                }
574            }
575        }
576
577        private Integer getVisibleRelationIndex(Relation rel) {
578            int i = getVisibleRelations().indexOf(rel);
579            if (i < 0)
580                return null;
581            return i;
582        }
583
584        public void updateTitle() {
585            if (!relations.isEmpty() && relations.size() != getSize()) {
586                RelationListDialog.this.setTitle(tr("Relations: {0}/{1}", getSize(), relations.size()));
587            } else if (getSize() > 0) {
588                RelationListDialog.this.setTitle(tr("Relations: {0}", getSize()));
589            } else {
590                RelationListDialog.this.setTitle(tr("Relations"));
591            }
592        }
593    }
594
595    private void setupPopupMenuHandler() {
596
597        // -- select action
598        popupMenuHandler.addAction(selectRelationAction);
599        popupMenuHandler.addAction(addRelationToSelectionAction);
600
601        // -- select members action
602        popupMenuHandler.addAction(selectMembersAction);
603        popupMenuHandler.addAction(addMembersToSelectionAction);
604
605        popupMenuHandler.addSeparator();
606        // -- download members action
607        popupMenuHandler.addAction(downloadMembersAction);
608
609        // -- download incomplete members action
610        popupMenuHandler.addAction(downloadSelectedIncompleteMembersAction);
611
612        popupMenuHandler.addSeparator();
613        popupMenuHandler.addAction(editAction).setVisible(false);
614        popupMenuHandler.addAction(duplicateAction).setVisible(false);
615        popupMenuHandler.addAction(deleteRelationsAction).setVisible(false);
616
617        popupMenuHandler.addAction(addSelectionToRelations);
618    }
619
620    /* ---------------------------------------------------------------------------------- */
621    /* Methods that can be called from plugins                                            */
622    /* ---------------------------------------------------------------------------------- */
623
624    /**
625     * Replies the popup menu handler.
626     * @return The popup menu handler
627     */
628    public PopupMenuHandler getPopupMenuHandler() {
629        return popupMenuHandler;
630    }
631
632    /**
633     * Replies the list of selected relations. Empty list, if there are no selected relations.
634     * @return the list of selected, non-new relations.
635     */
636    public Collection<Relation> getSelectedRelations() {
637        return model.getSelectedRelations();
638    }
639
640    /* ---------------------------------------------------------------------------------- */
641    /* DataSetListener                                                                    */
642    /* ---------------------------------------------------------------------------------- */
643
644    @Override
645    public void nodeMoved(NodeMovedEvent event) {
646        /* irrelevant in this context */
647    }
648
649    @Override
650    public void wayNodesChanged(WayNodesChangedEvent event) {
651        /* irrelevant in this context */
652    }
653
654    @Override
655    public void primitivesAdded(final PrimitivesAddedEvent event) {
656        model.addRelations(event.getPrimitives());
657        model.updateTitle();
658    }
659
660    @Override
661    public void primitivesRemoved(final PrimitivesRemovedEvent event) {
662        model.removeRelations(event.getPrimitives());
663        model.updateTitle();
664    }
665
666    @Override
667    public void relationMembersChanged(final RelationMembersChangedEvent event) {
668        List<Relation> sel = model.getSelectedRelations();
669        model.sort();
670        model.setSelectedRelations(sel);
671        displaylist.repaint();
672    }
673
674    @Override
675    public void tagsChanged(TagsChangedEvent event) {
676        OsmPrimitive prim = event.getPrimitive();
677        if (!(prim instanceof Relation))
678            return;
679        // trigger a sort of the relation list because the display name may have changed
680        //
681        List<Relation> sel = model.getSelectedRelations();
682        model.sort();
683        model.setSelectedRelations(sel);
684        displaylist.repaint();
685    }
686
687    @Override
688    public void dataChanged(DataChangedEvent event) {
689        initFromLayer(Main.main.getEditLayer());
690    }
691
692    @Override
693    public void otherDatasetChange(AbstractDatasetChangedEvent event) {
694        /* ignore */
695    }
696}
Note: See TracBrowser for help on using the repository browser.