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.relation.DownloadSelectedIncompleteMembersAction;
042import org.openstreetmap.josm.actions.relation.EditRelationAction;
043import org.openstreetmap.josm.actions.relation.SelectInRelationListAction;
044import org.openstreetmap.josm.data.SelectionChangedListener;
045import org.openstreetmap.josm.data.osm.DataSet;
046import org.openstreetmap.josm.data.osm.DefaultNameFormatter;
047import org.openstreetmap.josm.data.osm.Node;
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.PrimitiveRenderer;
068import org.openstreetmap.josm.gui.PopupMenuHandler;
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.history.HistoryBrowserDialogManager;
073import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeEvent;
074import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeListener;
075import org.openstreetmap.josm.gui.util.GuiHelper;
076import org.openstreetmap.josm.gui.util.HighlightHelper;
077import org.openstreetmap.josm.gui.widgets.ListPopupMenu;
078import org.openstreetmap.josm.gui.widgets.PopupMenuLauncher;
079import org.openstreetmap.josm.spi.preferences.Config;
080import org.openstreetmap.josm.tools.ImageProvider;
081import org.openstreetmap.josm.tools.InputMapUtils;
082import org.openstreetmap.josm.tools.Shortcut;
083import org.openstreetmap.josm.tools.Utils;
084import org.openstreetmap.josm.tools.bugreport.BugReport;
085
086/**
087 * A small tool dialog for displaying the current selection.
088 * @since 8
089 */
090public class SelectionListDialog extends ToggleDialog {
091    private JList<OsmPrimitive> lstPrimitives;
092    private final DefaultListSelectionModel selectionModel = new DefaultListSelectionModel();
093    private final SelectionListModel model = new SelectionListModel(selectionModel);
094
095    private final SelectAction actSelect = new SelectAction();
096    private final SearchAction actSearch = new SearchAction();
097    private final ShowHistoryAction actShowHistory = new ShowHistoryAction();
098    private final ZoomToJOSMSelectionAction actZoomToJOSMSelection = new ZoomToJOSMSelectionAction();
099    private final ZoomToListSelection actZoomToListSelection = new ZoomToListSelection();
100    private final SelectInRelationListAction actSetRelationSelection = new SelectInRelationListAction();
101    private final EditRelationAction actEditRelationSelection = new EditRelationAction();
102    private final DownloadSelectedIncompleteMembersAction actDownloadSelIncompleteMembers = new DownloadSelectedIncompleteMembersAction();
103
104    /** the popup menu and its handler */
105    private final ListPopupMenu popupMenu;
106    private final transient PopupMenuHandler popupMenuHandler;
107
108    /**
109     * Builds the content panel for this dialog
110     */
111    protected void buildContentPanel() {
112        lstPrimitives = new JList<>(model);
113        lstPrimitives.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
114        lstPrimitives.setSelectionModel(selectionModel);
115        lstPrimitives.setCellRenderer(new PrimitiveRenderer());
116        lstPrimitives.setTransferHandler(new SelectionTransferHandler());
117        if (!GraphicsEnvironment.isHeadless()) {
118            lstPrimitives.setDragEnabled(true);
119        }
120
121        lstPrimitives.getSelectionModel().addListSelectionListener(actSelect);
122        lstPrimitives.getSelectionModel().addListSelectionListener(actShowHistory);
123
124        // the select action
125        final SideButton selectButton = new SideButton(actSelect);
126        selectButton.createArrow(e -> SelectionHistoryPopup.launch(selectButton, model.getSelectionHistory()));
127
128        // the search button
129        final SideButton searchButton = new SideButton(actSearch);
130        searchButton.createArrow(e -> SearchPopupMenu.launch(searchButton), true);
131
132        createLayout(lstPrimitives, true, Arrays.asList(
133            selectButton, searchButton, new SideButton(actShowHistory)
134        ));
135    }
136
137    /**
138     * Constructs a new {@code SelectionListDialog}.
139     */
140    public SelectionListDialog() {
141        super(tr("Selection"), "selectionlist", tr("Open a selection list window."),
142                Shortcut.registerShortcut("subwindow:selection", tr("Toggle: {0}",
143                tr("Current Selection")), KeyEvent.VK_T, Shortcut.ALT_SHIFT),
144                150, // default height
145                true // default is "show dialog"
146        );
147
148        buildContentPanel();
149        model.addListDataListener(new TitleUpdater());
150        model.addListDataListener(actZoomToJOSMSelection);
151
152        popupMenu = new ListPopupMenu(lstPrimitives);
153        popupMenuHandler = setupPopupMenuHandler();
154
155        lstPrimitives.addListSelectionListener(e -> {
156            actZoomToListSelection.valueChanged(e);
157            popupMenuHandler.setPrimitives(model.getSelected());
158        });
159
160        lstPrimitives.addMouseListener(new MouseEventHandler());
161
162        InputMapUtils.addEnterAction(lstPrimitives, actZoomToListSelection);
163    }
164
165    @Override
166    public void showNotify() {
167        SelectionEventManager.getInstance().addSelectionListener(actShowHistory, FireMode.IN_EDT_CONSOLIDATED);
168        SelectionEventManager.getInstance().addSelectionListener(model, FireMode.IN_EDT_CONSOLIDATED);
169        DatasetEventManager.getInstance().addDatasetListener(model, FireMode.IN_EDT);
170        MainApplication.getLayerManager().addActiveLayerChangeListener(actSearch);
171        // editLayerChanged also gets the selection history of the level. Listener calls setJOSMSelection when fired.
172        MainApplication.getLayerManager().addAndFireActiveLayerChangeListener(model);
173        actSearch.updateEnabledState();
174    }
175
176    @Override
177    public void hideNotify() {
178        MainApplication.getLayerManager().removeActiveLayerChangeListener(actSearch);
179        MainApplication.getLayerManager().removeActiveLayerChangeListener(model);
180        SelectionEventManager.getInstance().removeSelectionListener(actShowHistory);
181        SelectionEventManager.getInstance().removeSelectionListener(model);
182        DatasetEventManager.getInstance().removeDatasetListener(model);
183    }
184
185    /**
186     * Responds to double clicks on the list of selected objects and launches the popup menu
187     */
188    class MouseEventHandler extends PopupMenuLauncher {
189        private final HighlightHelper helper = new HighlightHelper();
190        private final boolean highlightEnabled = Config.getPref().getBoolean("draw.target-highlight", true);
191
192        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                DataSet ds = MainApplication.getLayerManager().getActiveDataSet();
202                if (ds == null) return;
203                OsmPrimitive osm = model.getElementAt(idx);
204                Collection<OsmPrimitive> sel = ds.getSelected();
205                if (sel.size() != 1 || !sel.iterator().next().equals(osm)) {
206                    // Select primitive if it's not the whole current selection
207                    ds.setSelected(Collections.singleton(osm));
208                } else if (osm instanceof Relation) {
209                    // else open relation editor if applicable
210                    actEditRelationSelection.actionPerformed(null);
211                }
212            } else if (highlightEnabled && MainApplication.isDisplayingMapView() && helper.highlightOnly(model.getElementAt(idx))) {
213                MainApplication.getMap().mapView.repaint();
214            }
215        }
216
217        @Override
218        public void mouseExited(MouseEvent me) {
219            if (highlightEnabled) helper.clear();
220            super.mouseExited(me);
221        }
222    }
223
224    private PopupMenuHandler setupPopupMenuHandler() {
225        PopupMenuHandler handler = new PopupMenuHandler(popupMenu);
226        handler.addAction(actZoomToJOSMSelection);
227        handler.addAction(actZoomToListSelection);
228        handler.addSeparator();
229        handler.addAction(actSetRelationSelection);
230        handler.addAction(actEditRelationSelection);
231        handler.addSeparator();
232        handler.addAction(actDownloadSelIncompleteMembers);
233        return 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().getActiveDataSet() != 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            DataSet ds = MainApplication.getLayerManager().getActiveDataSet();
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, SelectionChangedListener {
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(Collection<? extends OsmPrimitive> newSelection) {
375            updateEnabledState(newSelection.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("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 box = new BoundingXYVisitor();
436            Collection<OsmPrimitive> sel = model.getSelected();
437            if (sel.isEmpty()) return;
438            box.computeBoundingBox(sel);
439            if (box.getBounds() == null)
440                return;
441            box.enlargeBoundingBox();
442            MainApplication.getMap().mapView.zoomTo(box);
443        }
444
445        protected void updateEnabledState() {
446            setEnabled(!model.isSelectionEmpty());
447        }
448
449        @Override
450        public void valueChanged(ListSelectionEvent e) {
451            updateEnabledState();
452        }
453    }
454
455    /**
456     * The list model for the list of OSM primitives in the current JOSM selection.
457     *
458     * The model also maintains a history of the last {@link SelectionListModel#SELECTION_HISTORY_SIZE}
459     * JOSM selection.
460     *
461     */
462    static class SelectionListModel extends AbstractListModel<OsmPrimitive>
463    implements ActiveLayerChangeListener, SelectionChangedListener, DataSetListener {
464
465        private static final int SELECTION_HISTORY_SIZE = 10;
466
467        // Variable to store history from currentDataSet()
468        private LinkedList<Collection<? extends OsmPrimitive>> history;
469        private final transient List<OsmPrimitive> selection = new ArrayList<>();
470        private final DefaultListSelectionModel selectionModel;
471
472        /**
473         * Constructor
474         * @param selectionModel the selection model used in the list
475         */
476        SelectionListModel(DefaultListSelectionModel selectionModel) {
477            this.selectionModel = selectionModel;
478        }
479
480        /**
481         * Replies a summary of the current JOSM selection
482         *
483         * @return a summary of the current JOSM selection
484         */
485        public synchronized String getJOSMSelectionSummary() {
486            if (selection.isEmpty()) return tr("Selection");
487            int numNodes = 0;
488            int numWays = 0;
489            int numRelations = 0;
490            for (OsmPrimitive p: selection) {
491                switch(p.getType()) {
492                case NODE: numNodes++; break;
493                case WAY: numWays++; break;
494                case RELATION: numRelations++; break;
495                default: throw new AssertionError();
496                }
497            }
498            return tr("Sel.: Rel.:{0} / Ways:{1} / Nodes:{2}", numRelations, numWays, numNodes);
499        }
500
501        /**
502         * Remembers a JOSM selection the history of JOSM selections
503         *
504         * @param selection the JOSM selection. Ignored if null or empty.
505         */
506        public void remember(Collection<? extends OsmPrimitive> selection) {
507            if (selection == null) return;
508            if (selection.isEmpty()) return;
509            if (history == null) return;
510            if (history.isEmpty()) {
511                history.add(selection);
512                return;
513            }
514            if (history.getFirst().equals(selection)) return;
515            history.addFirst(selection);
516            for (int i = 1; i < history.size(); ++i) {
517                if (history.get(i).equals(selection)) {
518                    history.remove(i);
519                    break;
520                }
521            }
522            int maxsize = Config.getPref().getInt("select.history-size", SELECTION_HISTORY_SIZE);
523            while (history.size() > maxsize) {
524                history.removeLast();
525            }
526        }
527
528        /**
529         * Replies the history of JOSM selections
530         *
531         * @return history of JOSM selections
532         */
533        public List<Collection<? extends OsmPrimitive>> getSelectionHistory() {
534            return history;
535        }
536
537        @Override
538        public synchronized OsmPrimitive getElementAt(int index) {
539            return selection.get(index);
540        }
541
542        @Override
543        public synchronized int getSize() {
544            return selection.size();
545        }
546
547        /**
548         * Determines if no OSM primitives are currently selected.
549         * @return {@code true} if no OSM primitives are currently selected
550         * @since 10383
551         */
552        public boolean isSelectionEmpty() {
553            return selectionModel.isSelectionEmpty();
554        }
555
556        /**
557         * Replies the collection of OSM primitives currently selected in the view of this model
558         *
559         * @return choosen elements in the view
560         */
561        public synchronized Collection<OsmPrimitive> getSelected() {
562            Set<OsmPrimitive> sel = new HashSet<>();
563            for (int i = 0; i < getSize(); i++) {
564                if (selectionModel.isSelectedIndex(i)) {
565                    sel.add(selection.get(i));
566                }
567            }
568            return sel;
569        }
570
571        /**
572         * Sets the OSM primitives to be selected in the view of this model
573         *
574         * @param sel the collection of primitives to select
575         */
576        public synchronized void setSelected(Collection<OsmPrimitive> sel) {
577            selectionModel.setValueIsAdjusting(true);
578            selectionModel.clearSelection();
579            if (sel != null) {
580                for (OsmPrimitive p: sel) {
581                    int i = selection.indexOf(p);
582                    if (i >= 0) {
583                        selectionModel.addSelectionInterval(i, i);
584                    }
585                }
586            }
587            selectionModel.setValueIsAdjusting(false);
588        }
589
590        @Override
591        protected void fireContentsChanged(Object source, int index0, int index1) {
592            Collection<OsmPrimitive> sel = getSelected();
593            super.fireContentsChanged(source, index0, index1);
594            setSelected(sel);
595        }
596
597        /**
598         * Sets the collection of currently selected OSM objects
599         *
600         * @param selection the collection of currently selected OSM objects
601         */
602        public void setJOSMSelection(final Collection<? extends OsmPrimitive> selection) {
603            synchronized (this) {
604                this.selection.clear();
605                if (selection != null) {
606                    this.selection.addAll(selection);
607                    sort();
608                }
609            }
610            GuiHelper.runInEDTAndWait(new Runnable() {
611                @Override public void run() {
612                    fireContentsChanged(this, 0, getSize());
613                    if (selection != null) {
614                        remember(selection);
615                    }
616                }
617            });
618        }
619
620        /**
621         * Triggers a refresh of the view for all primitives in {@code toUpdate}
622         * which are currently displayed in the view
623         *
624         * @param toUpdate the collection of primitives to update
625         */
626        public synchronized void update(Collection<? extends OsmPrimitive> toUpdate) {
627            if (toUpdate == null) return;
628            if (toUpdate.isEmpty()) return;
629            Collection<OsmPrimitive> sel = getSelected();
630            for (OsmPrimitive p: toUpdate) {
631                int i = selection.indexOf(p);
632                if (i >= 0) {
633                    super.fireContentsChanged(this, i, i);
634                }
635            }
636            setSelected(sel);
637        }
638
639        /**
640         * Sorts the current elements in the selection
641         */
642        public synchronized void sort() {
643            int size = selection.size();
644            if (size > 1 && size <= Config.getPref().getInt("selection.no_sort_above", 100_000)) {
645                boolean quick = size > Config.getPref().getInt("selection.fast_sort_above", 10_000);
646                Comparator<OsmPrimitive> c = Config.getPref().getBoolean("selection.sort_relations_before_ways", true)
647                        ? OsmPrimitiveComparator.orderingRelationsWaysNodes()
648                        : OsmPrimitiveComparator.orderingWaysRelationsNodes();
649                try {
650                    selection.sort(c.thenComparing(quick
651                            ? OsmPrimitiveComparator.comparingUniqueId()
652                            : OsmPrimitiveComparator.comparingNames()));
653                } catch (IllegalArgumentException e) {
654                    throw BugReport.intercept(e).put("size", size).put("quick", quick).put("selection", selection);
655                }
656            }
657        }
658
659        /* ------------------------------------------------------------------------ */
660        /* interface ActiveLayerChangeListener                                      */
661        /* ------------------------------------------------------------------------ */
662        @Override
663        public void activeOrEditLayerChanged(ActiveLayerChangeEvent e) {
664            DataSet newData = e.getSource().getEditDataSet();
665            if (newData == null) {
666                setJOSMSelection(null);
667                history = null;
668            } else {
669                history = newData.getSelectionHistory();
670                setJOSMSelection(newData.getAllSelected());
671            }
672        }
673
674        /* ------------------------------------------------------------------------ */
675        /* interface SelectionChangedListener                                       */
676        /* ------------------------------------------------------------------------ */
677        @Override
678        public void selectionChanged(Collection<? extends OsmPrimitive> newSelection) {
679            setJOSMSelection(newSelection);
680        }
681
682        /* ------------------------------------------------------------------------ */
683        /* interface DataSetListener                                                */
684        /* ------------------------------------------------------------------------ */
685        @Override
686        public void dataChanged(DataChangedEvent event) {
687            // refresh the whole list
688            fireContentsChanged(this, 0, getSize());
689        }
690
691        @Override
692        public void nodeMoved(NodeMovedEvent event) {
693            // may influence the display name of primitives, update the data
694            update(event.getPrimitives());
695        }
696
697        @Override
698        public void otherDatasetChange(AbstractDatasetChangedEvent event) {
699            // may influence the display name of primitives, update the data
700            update(event.getPrimitives());
701        }
702
703        @Override
704        public void relationMembersChanged(RelationMembersChangedEvent event) {
705            // may influence the display name of primitives, update the data
706            update(event.getPrimitives());
707        }
708
709        @Override
710        public void tagsChanged(TagsChangedEvent event) {
711            // may influence the display name of primitives, update the data
712            update(event.getPrimitives());
713        }
714
715        @Override
716        public void wayNodesChanged(WayNodesChangedEvent event) {
717            // may influence the display name of primitives, update the data
718            update(event.getPrimitives());
719        }
720
721        @Override
722        public void primitivesAdded(PrimitivesAddedEvent event) {
723            /* ignored - handled by SelectionChangeListener */
724        }
725
726        @Override
727        public void primitivesRemoved(PrimitivesRemovedEvent event) {
728            /* ignored - handled by SelectionChangeListener*/
729        }
730    }
731
732    /**
733     * A specialized {@link JMenuItem} for presenting one entry of the search history
734     *
735     * @author Jan Peter Stotz
736     */
737    protected static class SearchMenuItem extends JMenuItem implements ActionListener {
738        protected final transient SearchSetting s;
739
740        public SearchMenuItem(SearchSetting s) {
741            super(Utils.shortenString(s.toString(),
742                    org.openstreetmap.josm.actions.search.SearchAction.MAX_LENGTH_SEARCH_EXPRESSION_DISPLAY));
743            this.s = s;
744            addActionListener(this);
745        }
746
747        @Override
748        public void actionPerformed(ActionEvent e) {
749            org.openstreetmap.josm.actions.search.SearchAction.searchWithoutHistory(s);
750        }
751    }
752
753    /**
754     * The popup menu for the search history entries
755     *
756     */
757    protected static class SearchPopupMenu extends JPopupMenu {
758        public static void launch(Component parent) {
759            if (org.openstreetmap.josm.actions.search.SearchAction.getSearchHistory().isEmpty())
760                return;
761            JPopupMenu menu = new SearchPopupMenu();
762            Rectangle r = parent.getBounds();
763            menu.show(parent, r.x, r.y + r.height);
764        }
765
766        /**
767         * Constructs a new {@code SearchPopupMenu}.
768         */
769        public SearchPopupMenu() {
770            for (SearchSetting ss: org.openstreetmap.josm.actions.search.SearchAction.getSearchHistory()) {
771                add(new SearchMenuItem(ss));
772            }
773        }
774    }
775
776    /**
777     * A specialized {@link JMenuItem} for presenting one entry of the selection history
778     *
779     * @author Jan Peter Stotz
780     */
781    protected static class SelectionMenuItem extends JMenuItem implements ActionListener {
782        protected transient Collection<? extends OsmPrimitive> sel;
783
784        public SelectionMenuItem(Collection<? extends OsmPrimitive> sel) {
785            this.sel = sel;
786            int ways = 0;
787            int nodes = 0;
788            int relations = 0;
789            for (OsmPrimitive o : sel) {
790                if (!o.isSelectable()) continue; // skip unselectable primitives
791                if (o instanceof Way) {
792                    ways++;
793                } else if (o instanceof Node) {
794                    nodes++;
795                } else if (o instanceof Relation) {
796                    relations++;
797                }
798            }
799            StringBuilder text = new StringBuilder();
800            if (ways != 0) {
801                text.append(text.length() > 0 ? ", " : "")
802                .append(trn("{0} way", "{0} ways", ways, ways));
803            }
804            if (nodes != 0) {
805                text.append(text.length() > 0 ? ", " : "")
806                .append(trn("{0} node", "{0} nodes", nodes, nodes));
807            }
808            if (relations != 0) {
809                text.append(text.length() > 0 ? ", " : "")
810                .append(trn("{0} relation", "{0} relations", relations, relations));
811            }
812            if (ways + nodes + relations == 0) {
813                text.append(tr("Unselectable now"));
814                this.sel = new ArrayList<>(); // empty selection
815            }
816            DefaultNameFormatter df = DefaultNameFormatter.getInstance();
817            if (ways + nodes + relations == 1) {
818                text.append(": ");
819                for (OsmPrimitive o : sel) {
820                    text.append(o.getDisplayName(df));
821                }
822                setText(text.toString());
823            } else {
824                setText(tr("Selection: {0}", text));
825            }
826            addActionListener(this);
827        }
828
829        @Override
830        public void actionPerformed(ActionEvent e) {
831            MainApplication.getLayerManager().getActiveDataSet().setSelected(sel);
832        }
833    }
834
835    /**
836     * The popup menu for the JOSM selection history entries
837     */
838    protected static class SelectionHistoryPopup extends JPopupMenu {
839        public static void launch(Component parent, Collection<Collection<? extends OsmPrimitive>> history) {
840            if (history == null || history.isEmpty()) return;
841            JPopupMenu menu = new SelectionHistoryPopup(history);
842            Rectangle r = parent.getBounds();
843            menu.show(parent, r.x, r.y + r.height);
844        }
845
846        public SelectionHistoryPopup(Collection<Collection<? extends OsmPrimitive>> history) {
847            for (Collection<? extends OsmPrimitive> sel : history) {
848                add(new SelectionMenuItem(sel));
849            }
850        }
851    }
852
853    /**
854     * A transfer handler class for drag-and-drop support.
855     */
856    protected class SelectionTransferHandler extends TransferHandler {
857
858        @Override
859        public int getSourceActions(JComponent c) {
860            return COPY;
861        }
862
863        @Override
864        protected Transferable createTransferable(JComponent c) {
865            return new PrimitiveTransferable(PrimitiveTransferData.getData(getSelectedPrimitives()));
866        }
867    }
868}