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

Last change on this file since 7539 was 7539, checked in by Don-vip, 5 years ago

fix #5957, fix #10458, see #10455: fix various focus/shortcuts issues:

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