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