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

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