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