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