001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.dialogs;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005import static org.openstreetmap.josm.tools.I18n.trn;
006
007import java.awt.Component;
008import java.awt.Rectangle;
009import java.awt.event.ActionEvent;
010import java.awt.event.ActionListener;
011import java.awt.event.KeyEvent;
012import java.awt.event.MouseEvent;
013import java.util.ArrayList;
014import java.util.Arrays;
015import java.util.Collection;
016import java.util.Collections;
017import java.util.HashSet;
018import java.util.Iterator;
019import java.util.LinkedList;
020import java.util.List;
021import java.util.Set;
022
023import javax.swing.AbstractAction;
024import javax.swing.AbstractListModel;
025import javax.swing.DefaultListSelectionModel;
026import javax.swing.JList;
027import javax.swing.JMenuItem;
028import javax.swing.JPopupMenu;
029import javax.swing.ListSelectionModel;
030import javax.swing.event.ListDataEvent;
031import javax.swing.event.ListDataListener;
032import javax.swing.event.ListSelectionEvent;
033import javax.swing.event.ListSelectionListener;
034
035import org.openstreetmap.josm.Main;
036import org.openstreetmap.josm.actions.AutoScaleAction;
037import org.openstreetmap.josm.actions.relation.DownloadSelectedIncompleteMembersAction;
038import org.openstreetmap.josm.actions.relation.EditRelationAction;
039import org.openstreetmap.josm.actions.relation.SelectInRelationListAction;
040import org.openstreetmap.josm.actions.search.SearchAction.SearchSetting;
041import org.openstreetmap.josm.data.SelectionChangedListener;
042import org.openstreetmap.josm.data.osm.Node;
043import org.openstreetmap.josm.data.osm.OsmPrimitive;
044import org.openstreetmap.josm.data.osm.OsmPrimitiveComparator;
045import org.openstreetmap.josm.data.osm.Relation;
046import org.openstreetmap.josm.data.osm.Way;
047import org.openstreetmap.josm.data.osm.event.AbstractDatasetChangedEvent;
048import org.openstreetmap.josm.data.osm.event.DataChangedEvent;
049import org.openstreetmap.josm.data.osm.event.DataSetListener;
050import org.openstreetmap.josm.data.osm.event.DatasetEventManager;
051import org.openstreetmap.josm.data.osm.event.DatasetEventManager.FireMode;
052import org.openstreetmap.josm.data.osm.event.NodeMovedEvent;
053import org.openstreetmap.josm.data.osm.event.PrimitivesAddedEvent;
054import org.openstreetmap.josm.data.osm.event.PrimitivesRemovedEvent;
055import org.openstreetmap.josm.data.osm.event.RelationMembersChangedEvent;
056import org.openstreetmap.josm.data.osm.event.SelectionEventManager;
057import org.openstreetmap.josm.data.osm.event.TagsChangedEvent;
058import org.openstreetmap.josm.data.osm.event.WayNodesChangedEvent;
059import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor;
060import org.openstreetmap.josm.gui.DefaultNameFormatter;
061import org.openstreetmap.josm.gui.MapView;
062import org.openstreetmap.josm.gui.MapView.EditLayerChangeListener;
063import org.openstreetmap.josm.gui.OsmPrimitivRenderer;
064import org.openstreetmap.josm.gui.PopupMenuHandler;
065import org.openstreetmap.josm.gui.SideButton;
066import org.openstreetmap.josm.gui.layer.OsmDataLayer;
067import org.openstreetmap.josm.gui.util.GuiHelper;
068import org.openstreetmap.josm.gui.util.HighlightHelper;
069import org.openstreetmap.josm.gui.widgets.ListPopupMenu;
070import org.openstreetmap.josm.gui.widgets.PopupMenuLauncher;
071import org.openstreetmap.josm.tools.ImageProvider;
072import org.openstreetmap.josm.tools.InputMapUtils;
073import org.openstreetmap.josm.tools.Shortcut;
074import org.openstreetmap.josm.tools.SubclassFilteredCollection;
075import org.openstreetmap.josm.tools.Utils;
076
077/**
078 * A small tool dialog for displaying the current selection.
079 * @since 8
080 */
081public class SelectionListDialog extends ToggleDialog  {
082    private JList<OsmPrimitive> lstPrimitives;
083    private DefaultListSelectionModel selectionModel  = new DefaultListSelectionModel();
084    private SelectionListModel model = new SelectionListModel(selectionModel);
085
086    private SelectAction actSelect = new SelectAction();
087    private SearchAction actSearch = new SearchAction();
088    private ZoomToJOSMSelectionAction actZoomToJOSMSelection = new ZoomToJOSMSelectionAction();
089    private ZoomToListSelection actZoomToListSelection = new ZoomToListSelection();
090    private SelectInRelationListAction actSetRelationSelection = new SelectInRelationListAction();
091    private EditRelationAction actEditRelationSelection = new EditRelationAction();
092    private DownloadSelectedIncompleteMembersAction actDownloadSelectedIncompleteMembers = new DownloadSelectedIncompleteMembersAction();
093
094    /** the popup menu and its handler */
095    private final ListPopupMenu popupMenu;
096    private final PopupMenuHandler popupMenuHandler;
097
098    /**
099     * Builds the content panel for this dialog
100     */
101    protected void buildContentPanel() {
102        lstPrimitives = new JList<>(model);
103        lstPrimitives.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
104        lstPrimitives.setSelectionModel(selectionModel);
105        lstPrimitives.setCellRenderer(new OsmPrimitivRenderer());
106        lstPrimitives.setTransferHandler(null); // Fix #6290. Drag & Drop is not supported anyway and Copy/Paste is better propagated to main window
107
108        // the select action
109        final SideButton selectButton = new SideButton(actSelect);
110        lstPrimitives.getSelectionModel().addListSelectionListener(actSelect);
111        selectButton.createArrow(new ActionListener() {
112            @Override
113            public void actionPerformed(ActionEvent e) {
114                SelectionHistoryPopup.launch(selectButton, model.getSelectionHistory());
115            }
116        });
117
118        // the search button
119        final SideButton searchButton = new SideButton(actSearch);
120        searchButton.createArrow(new ActionListener() {
121            @Override
122            public void actionPerformed(ActionEvent e) {
123                SearchPopupMenu.launch(searchButton);
124            }
125        });
126
127        createLayout(lstPrimitives, true, Arrays.asList(new SideButton[] {
128            selectButton, searchButton
129        }));
130    }
131
132    /**
133     * Constructs a new {@code SelectionListDialog}.
134     */
135    public SelectionListDialog() {
136        super(tr("Selection"), "selectionlist", tr("Open a selection list window."),
137                Shortcut.registerShortcut("subwindow:selection", tr("Toggle: {0}",
138                tr("Current Selection")), KeyEvent.VK_T, Shortcut.ALT_SHIFT),
139                150, // default height
140                true // default is "show dialog"
141        );
142
143        buildContentPanel();
144        model.addListDataListener(new TitleUpdater());
145        model.addListDataListener(actZoomToJOSMSelection);
146
147        popupMenu = new ListPopupMenu(lstPrimitives);
148        popupMenuHandler = setupPopupMenuHandler();
149
150        lstPrimitives.addListSelectionListener(new ListSelectionListener() {
151            @Override
152            public void valueChanged(ListSelectionEvent e) {
153                actZoomToListSelection.valueChanged(e);
154                popupMenuHandler.setPrimitives(model.getSelected());
155            }
156        });
157
158        lstPrimitives.addMouseListener(new MouseEventHandler());
159
160        InputMapUtils.addEnterAction(lstPrimitives, actZoomToListSelection);
161    }
162
163    @Override
164    public void showNotify() {
165        MapView.addEditLayerChangeListener(model);
166        SelectionEventManager.getInstance().addSelectionListener(model, FireMode.IN_EDT_CONSOLIDATED);
167        DatasetEventManager.getInstance().addDatasetListener(model, FireMode.IN_EDT);
168        MapView.addEditLayerChangeListener(actSearch);
169        // editLayerChanged also gets the selection history of the level
170        OsmDataLayer editLayer = Main.main.getEditLayer();
171        model.editLayerChanged(null, editLayer);
172        if (editLayer != null) {
173            model.setJOSMSelection(editLayer.data.getAllSelected());
174        }
175        actSearch.updateEnabledState();
176    }
177
178    @Override
179    public void hideNotify() {
180        MapView.removeEditLayerChangeListener(actSearch);
181        MapView.removeEditLayerChangeListener(model);
182        SelectionEventManager.getInstance().removeSelectionListener(model);
183        DatasetEventManager.getInstance().removeDatasetListener(model);
184    }
185
186    /**
187     * Responds to double clicks on the list of selected objects and launches the popup menu
188     */
189    class MouseEventHandler extends PopupMenuLauncher {
190        private final HighlightHelper helper = new HighlightHelper();
191        private boolean highlightEnabled = Main.pref.getBoolean("draw.target-highlight", true);
192        public MouseEventHandler() {
193            super(popupMenu);
194        }
195
196        @Override
197        public void mouseClicked(MouseEvent e) {
198            int idx = lstPrimitives.locationToIndex(e.getPoint());
199            if (idx < 0) return;
200            if (isDoubleClick(e)) {
201                OsmDataLayer layer = Main.main.getEditLayer();
202                if (layer == null) return;
203                layer.data.setSelected(Collections.singleton(model.getElementAt(idx)));
204            } else if (highlightEnabled && Main.isDisplayingMapView()) {
205                if (helper.highlightOnly(model.getElementAt(idx))) {
206                    Main.map.mapView.repaint();
207                }
208            }
209        }
210
211        @Override
212        public void mouseExited(MouseEvent me) {
213            if (highlightEnabled) helper.clear();
214            super.mouseExited(me);
215        }
216    }
217
218    private PopupMenuHandler setupPopupMenuHandler() {
219        PopupMenuHandler handler = new PopupMenuHandler(popupMenu);
220        handler.addAction(actZoomToJOSMSelection);
221        handler.addAction(actZoomToListSelection);
222        handler.addSeparator();
223        handler.addAction(actSetRelationSelection);
224        handler.addAction(actEditRelationSelection);
225        handler.addSeparator();
226        handler.addAction(actDownloadSelectedIncompleteMembers);
227        return handler;
228    }
229
230    /**
231     * Replies the popup menu handler.
232     * @return The popup menu handler
233     */
234    public PopupMenuHandler getPopupMenuHandler() {
235        return popupMenuHandler;
236    }
237
238    /**
239     * Replies the selected OSM primitives.
240     * @return The selected OSM primitives
241     */
242    public Collection<OsmPrimitive> getSelectedPrimitives() {
243        return model.getSelected();
244    }
245
246    /**
247     * Updates the dialog title with a summary of the current JOSM selection
248     */
249    class TitleUpdater implements ListDataListener {
250        protected void updateTitle() {
251            setTitle(model.getJOSMSelectionSummary());
252        }
253
254        @Override
255        public void contentsChanged(ListDataEvent e) {
256            updateTitle();
257        }
258
259        @Override
260        public void intervalAdded(ListDataEvent e) {
261            updateTitle();
262        }
263
264        @Override
265        public void intervalRemoved(ListDataEvent e) {
266            updateTitle();
267        }
268    }
269
270    /**
271     * Launches the search dialog
272     */
273    static class SearchAction extends AbstractAction implements EditLayerChangeListener {
274        public SearchAction() {
275            putValue(NAME, tr("Search"));
276            putValue(SHORT_DESCRIPTION,   tr("Search for objects"));
277            putValue(SMALL_ICON, ImageProvider.get("dialogs","search"));
278            updateEnabledState();
279        }
280
281        @Override
282        public void actionPerformed(ActionEvent e) {
283            if (!isEnabled()) return;
284            org.openstreetmap.josm.actions.search.SearchAction.search();
285        }
286
287        public void updateEnabledState() {
288            setEnabled(Main.main != null && Main.main.hasEditLayer());
289        }
290
291        @Override
292        public void editLayerChanged(OsmDataLayer oldLayer, OsmDataLayer newLayer) {
293            updateEnabledState();
294        }
295    }
296
297    /**
298     * Sets the current JOSM selection to the OSM primitives selected in the list
299     * of this dialog
300     */
301    class SelectAction extends AbstractAction implements ListSelectionListener {
302        public SelectAction() {
303            putValue(NAME, tr("Select"));
304            putValue(SHORT_DESCRIPTION,  tr("Set the selected elements on the map to the selected items in the list above."));
305            putValue(SMALL_ICON, ImageProvider.get("dialogs","select"));
306            updateEnabledState();
307        }
308
309        @Override
310        public void actionPerformed(ActionEvent e) {
311            Collection<OsmPrimitive> sel = model.getSelected();
312            if (sel.isEmpty())return;
313            OsmDataLayer editLayer = Main.main.getEditLayer();
314            if (editLayer == null) return;
315            editLayer.data.setSelected(sel);
316        }
317
318        public void updateEnabledState() {
319            setEnabled(!model.getSelected().isEmpty());
320        }
321
322        @Override
323        public void valueChanged(ListSelectionEvent e) {
324            updateEnabledState();
325        }
326    }
327
328    /**
329     * The action for zooming to the primitives in the current JOSM selection
330     *
331     */
332    class ZoomToJOSMSelectionAction extends AbstractAction implements ListDataListener {
333
334        public ZoomToJOSMSelectionAction() {
335            putValue(NAME,tr("Zoom to selection"));
336            putValue(SHORT_DESCRIPTION, tr("Zoom to selection"));
337            putValue(SMALL_ICON, ImageProvider.get("dialogs/autoscale", "selection"));
338            updateEnabledState();
339        }
340
341        @Override
342        public void actionPerformed(ActionEvent e) {
343            AutoScaleAction.autoScale("selection");
344        }
345
346        public void updateEnabledState() {
347            setEnabled(model.getSize() > 0);
348        }
349
350        @Override
351        public void contentsChanged(ListDataEvent e) {
352            updateEnabledState();
353        }
354
355        @Override
356        public void intervalAdded(ListDataEvent e) {
357            updateEnabledState();
358        }
359
360        @Override
361        public void intervalRemoved(ListDataEvent e) {
362            updateEnabledState();
363        }
364    }
365
366    /**
367     * The action for zooming to the primitives which are currently selected in
368     * the list displaying the JOSM selection
369     *
370     */
371    class ZoomToListSelection extends AbstractAction implements ListSelectionListener{
372        public ZoomToListSelection() {
373            putValue(NAME, tr("Zoom to selected element(s)"));
374            putValue(SHORT_DESCRIPTION, tr("Zoom to selected element(s)"));
375            putValue(SMALL_ICON, ImageProvider.get("dialogs/autoscale", "selection"));
376            updateEnabledState();
377        }
378
379        @Override
380        public void actionPerformed(ActionEvent e) {
381            BoundingXYVisitor box = new BoundingXYVisitor();
382            Collection<OsmPrimitive> sel = model.getSelected();
383            if (sel.isEmpty()) return;
384            box.computeBoundingBox(sel);
385            if (box.getBounds() == null)
386                return;
387            box.enlargeBoundingBox();
388            Main.map.mapView.recalculateCenterScale(box);
389        }
390
391        public void updateEnabledState() {
392            setEnabled(!model.getSelected().isEmpty());
393        }
394
395        @Override
396        public void valueChanged(ListSelectionEvent e) {
397            updateEnabledState();
398        }
399    }
400
401    /**
402     * The list model for the list of OSM primitives in the current JOSM selection.
403     *
404     * The model also maintains a history of the last {@link SelectionListModel#SELECTION_HISTORY_SIZE}
405     * JOSM selection.
406     *
407     */
408    private static class SelectionListModel extends AbstractListModel<OsmPrimitive> implements EditLayerChangeListener, SelectionChangedListener, DataSetListener{
409
410        private static final int SELECTION_HISTORY_SIZE = 10;
411
412        // Variable to store history from currentDataSet()
413        private LinkedList<Collection<? extends OsmPrimitive>> history;
414        private final List<OsmPrimitive> selection = new ArrayList<>();
415        private DefaultListSelectionModel selectionModel;
416
417        /**
418         * Constructor
419         * @param selectionModel the selection model used in the list
420         */
421        public SelectionListModel(DefaultListSelectionModel selectionModel) {
422            this.selectionModel = selectionModel;
423        }
424
425        /**
426         * Replies a summary of the current JOSM selection
427         *
428         * @return a summary of the current JOSM selection
429         */
430        public String getJOSMSelectionSummary() {
431            if (selection.isEmpty()) return tr("Selection");
432            int numNodes = 0;
433            int numWays = 0;
434            int numRelations = 0;
435            for (OsmPrimitive p: selection) {
436                switch(p.getType()) {
437                case NODE: numNodes++; break;
438                case WAY: numWays++; break;
439                case RELATION: numRelations++; break;
440                }
441            }
442            return tr("Sel.: Rel.:{0} / Ways:{1} / Nodes:{2}", numRelations, numWays, numNodes);
443        }
444
445        /**
446         * Remembers a JOSM selection the history of JOSM selections
447         *
448         * @param selection the JOSM selection. Ignored if null or empty.
449         */
450        public void remember(Collection<? extends OsmPrimitive> selection) {
451            if (selection == null)return;
452            if (selection.isEmpty())return;
453            if (history == null) return;
454            if (history.isEmpty()) {
455                history.add(selection);
456                return;
457            }
458            if (history.getFirst().equals(selection)) return;
459            history.addFirst(selection);
460            for(int i = 1; i < history.size(); ++i) {
461                if(history.get(i).equals(selection)) {
462                    history.remove(i);
463                    break;
464                }
465            }
466            int maxsize = Main.pref.getInteger("select.history-size", SELECTION_HISTORY_SIZE);
467            while (history.size() > maxsize) {
468                history.removeLast();
469            }
470        }
471
472        /**
473         * Replies the history of JOSM selections
474         *
475         * @return history of JOSM selections
476         */
477        public List<Collection<? extends OsmPrimitive>> getSelectionHistory() {
478            return history;
479        }
480
481        @Override
482        public OsmPrimitive getElementAt(int index) {
483            return selection.get(index);
484        }
485
486        @Override
487        public int getSize() {
488            return selection.size();
489        }
490
491        /**
492         * Replies the collection of OSM primitives currently selected in the view
493         * of this model
494         *
495         * @return choosen elements in the view
496         */
497        public Collection<OsmPrimitive> getSelected() {
498            Set<OsmPrimitive> sel = new HashSet<>();
499            for(int i=0; i< getSize();i++) {
500                if (selectionModel.isSelectedIndex(i)) {
501                    sel.add(selection.get(i));
502                }
503            }
504            return sel;
505        }
506
507        /**
508         * Sets the OSM primitives to be selected in the view of this model
509         *
510         * @param sel the collection of primitives to select
511         */
512        public void setSelected(Collection<OsmPrimitive> sel) {
513            selectionModel.clearSelection();
514            if (sel == null) return;
515            for (OsmPrimitive p: sel){
516                int i = selection.indexOf(p);
517                if (i >= 0){
518                    selectionModel.addSelectionInterval(i, i);
519                }
520            }
521        }
522
523        @Override
524        protected void fireContentsChanged(Object source, int index0, int index1) {
525            Collection<OsmPrimitive> sel = getSelected();
526            super.fireContentsChanged(source, index0, index1);
527            setSelected(sel);
528        }
529
530        /**
531         * Sets the collection of currently selected OSM objects
532         *
533         * @param selection the collection of currently selected OSM objects
534         */
535        public void setJOSMSelection(final Collection<? extends OsmPrimitive> selection) {
536            this.selection.clear();
537            if (selection != null) {
538                this.selection.addAll(selection);
539                sort();
540            }
541            GuiHelper.runInEDTAndWait(new Runnable() {
542                @Override public void run() {
543                    fireContentsChanged(this, 0, getSize());
544                    if (selection != null) {
545                        remember(selection);
546                        if (selection.size() == 2) {
547                            Iterator<? extends OsmPrimitive> it = selection.iterator();
548                            OsmPrimitive n1 = it.next(), n2=it.next();
549                            // show distance between two selected nodes
550                            if (n1 instanceof Node && n2 instanceof Node) {
551                                double d = ((Node) n1).getCoor().greatCircleDistance(((Node) n2).getCoor());
552                                Main.map.statusLine.setDist(d);
553                                return;
554                            }
555                        }
556                        Main.map.statusLine.setDist(new SubclassFilteredCollection<OsmPrimitive, Way>(selection, OsmPrimitive.wayPredicate));
557                    }
558                }
559            });
560        }
561
562        /**
563         * Triggers a refresh of the view for all primitives in {@code toUpdate}
564         * which are currently displayed in the view
565         *
566         * @param toUpdate the collection of primitives to update
567         */
568        public void update(Collection<? extends OsmPrimitive> toUpdate) {
569            if (toUpdate == null) return;
570            if (toUpdate.isEmpty()) return;
571            Collection<OsmPrimitive> sel = getSelected();
572            for (OsmPrimitive p: toUpdate){
573                int i = selection.indexOf(p);
574                if (i >= 0) {
575                    super.fireContentsChanged(this, i,i);
576                }
577            }
578            setSelected(sel);
579        }
580
581        /**
582         * Sorts the current elements in the selection
583         */
584        public void sort() {
585            if (this.selection.size() <= Main.pref.getInteger("selection.no_sort_above", 100000)) {
586                boolean quick = this.selection.size() > Main.pref.getInteger("selection.fast_sort_above", 10000);
587                Collections.sort(this.selection, new OsmPrimitiveComparator(quick, false));
588            }
589        }
590
591        /* ------------------------------------------------------------------------ */
592        /* interface EditLayerChangeListener                                        */
593        /* ------------------------------------------------------------------------ */
594        @Override
595        public void editLayerChanged(OsmDataLayer oldLayer, OsmDataLayer newLayer) {
596            if (newLayer == null) {
597                setJOSMSelection(null);
598                history = null;
599            } else {
600                history = newLayer.data.getSelectionHistory();
601                setJOSMSelection(newLayer.data.getAllSelected());
602            }
603        }
604
605        /* ------------------------------------------------------------------------ */
606        /* interface SelectionChangeListener                                        */
607        /* ------------------------------------------------------------------------ */
608        @Override
609        public void selectionChanged(Collection<? extends OsmPrimitive> newSelection) {
610            setJOSMSelection(newSelection);
611        }
612
613        /* ------------------------------------------------------------------------ */
614        /* interface DataSetListener                                                */
615        /* ------------------------------------------------------------------------ */
616        @Override
617        public void dataChanged(DataChangedEvent event) {
618            // refresh the whole list
619            fireContentsChanged(this, 0, getSize());
620        }
621
622        @Override
623        public void nodeMoved(NodeMovedEvent event) {
624            // may influence the display name of primitives, update the data
625            update(event.getPrimitives());
626        }
627
628        @Override
629        public void otherDatasetChange(AbstractDatasetChangedEvent event) {
630            // may influence the display name of primitives, update the data
631            update(event.getPrimitives());
632        }
633
634        @Override
635        public void relationMembersChanged(RelationMembersChangedEvent event) {
636            // may influence the display name of primitives, update the data
637            update(event.getPrimitives());
638        }
639
640        @Override
641        public void tagsChanged(TagsChangedEvent event) {
642            // may influence the display name of primitives, update the data
643            update(event.getPrimitives());
644        }
645
646        @Override
647        public void wayNodesChanged(WayNodesChangedEvent event) {
648            // may influence the display name of primitives, update the data
649            update(event.getPrimitives());
650        }
651
652        @Override
653        public void primitivesAdded(PrimitivesAddedEvent event) {/* ignored - handled by SelectionChangeListener */}
654        @Override
655        public void primitivesRemoved(PrimitivesRemovedEvent event) {/* ignored - handled by SelectionChangeListener*/}
656    }
657
658    /**
659     * A specialized {@link JMenuItem} for presenting one entry of the search history
660     *
661     * @author Jan Peter Stotz
662     */
663    protected static class SearchMenuItem extends JMenuItem implements ActionListener {
664        protected final SearchSetting s;
665
666        public SearchMenuItem(SearchSetting s) {
667            super(Utils.shortenString(s.toString(), org.openstreetmap.josm.actions.search.SearchAction.MAX_LENGTH_SEARCH_EXPRESSION_DISPLAY));
668            this.s = s;
669            addActionListener(this);
670        }
671
672        @Override
673        public void actionPerformed(ActionEvent e) {
674            org.openstreetmap.josm.actions.search.SearchAction.searchWithoutHistory(s);
675        }
676    }
677
678    /**
679     * The popup menu for the search history entries
680     *
681     */
682    protected static class SearchPopupMenu extends JPopupMenu {
683        public static void launch(Component parent) {
684            if (org.openstreetmap.josm.actions.search.SearchAction.getSearchHistory().isEmpty())
685                return;
686            JPopupMenu menu = new SearchPopupMenu();
687            Rectangle r = parent.getBounds();
688            menu.show(parent, r.x, r.y + r.height);
689        }
690
691        public SearchPopupMenu() {
692            for (SearchSetting ss: org.openstreetmap.josm.actions.search.SearchAction.getSearchHistory()) {
693                add(new SearchMenuItem(ss));
694            }
695        }
696    }
697
698    /**
699     * A specialized {@link JMenuItem} for presenting one entry of the selection history
700     *
701     * @author Jan Peter Stotz
702     */
703    protected static class SelectionMenuItem extends JMenuItem implements ActionListener {
704        private final DefaultNameFormatter df = DefaultNameFormatter.getInstance();
705        protected Collection<? extends OsmPrimitive> sel;
706
707        public SelectionMenuItem(Collection<? extends OsmPrimitive> sel) {
708            super();
709            this.sel = sel;
710            int ways = 0;
711            int nodes = 0;
712            int relations = 0;
713            for (OsmPrimitive o : sel) {
714                if (! o.isSelectable()) continue; // skip unselectable primitives
715                if (o instanceof Way) {
716                    ways++;
717                } else if (o instanceof Node) {
718                    nodes++;
719                } else if (o instanceof Relation) {
720                    relations++;
721                }
722            }
723            StringBuilder text = new StringBuilder();
724            if(ways != 0) {
725                text.append(text.length() > 0 ? ", " : "")
726                .append(trn("{0} way", "{0} ways", ways, ways));
727            }
728            if(nodes != 0) {
729                text.append(text.length() > 0 ? ", " : "")
730                .append(trn("{0} node", "{0} nodes", nodes, nodes));
731            }
732            if(relations != 0) {
733                text.append(text.length() > 0 ? ", " : "")
734                .append(trn("{0} relation", "{0} relations", relations, relations));
735            }
736            if(ways + nodes + relations == 0) {
737                text.append(tr("Unselectable now"));
738                this.sel=new ArrayList<>(); // empty selection
739            }
740            if(ways + nodes + relations == 1)
741            {
742                text.append(": ");
743                for(OsmPrimitive o : sel) {
744                    text.append(o.getDisplayName(df));
745                }
746                setText(text.toString());
747            } else {
748                setText(tr("Selection: {0}", text));
749            }
750            addActionListener(this);
751        }
752
753        @Override
754        public void actionPerformed(ActionEvent e) {
755            Main.main.getCurrentDataSet().setSelected(sel);
756        }
757    }
758
759    /**
760     * The popup menu for the JOSM selection history entries
761     */
762    protected static class SelectionHistoryPopup extends JPopupMenu {
763        public static void launch(Component parent, Collection<Collection<? extends OsmPrimitive>> history) {
764            if (history == null || history.isEmpty()) return;
765            JPopupMenu menu = new SelectionHistoryPopup(history);
766            Rectangle r = parent.getBounds();
767            menu.show(parent, r.x, r.y + r.height);
768        }
769
770        public SelectionHistoryPopup(Collection<Collection<? extends OsmPrimitive>> history) {
771            for (Collection<? extends OsmPrimitive> sel : history) {
772                add(new SelectionMenuItem(sel));
773            }
774        }
775    }
776}