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() && helper.highlightOnly(model.getElementAt(idx))) { 214 Main.map.mapView.repaint(); 215 } 216 } 217 218 @Override 219 public void mouseExited(MouseEvent me) { 220 if (highlightEnabled) helper.clear(); 221 super.mouseExited(me); 222 } 223 } 224 225 private PopupMenuHandler setupPopupMenuHandler() { 226 PopupMenuHandler handler = new PopupMenuHandler(popupMenu); 227 handler.addAction(actZoomToJOSMSelection); 228 handler.addAction(actZoomToListSelection); 229 handler.addSeparator(); 230 handler.addAction(actSetRelationSelection); 231 handler.addAction(actEditRelationSelection); 232 handler.addSeparator(); 233 handler.addAction(actDownloadSelIncompleteMembers); 234 return handler; 235 } 236 237 /** 238 * Replies the popup menu handler. 239 * @return The popup menu handler 240 */ 241 public PopupMenuHandler getPopupMenuHandler() { 242 return popupMenuHandler; 243 } 244 245 /** 246 * Replies the selected OSM primitives. 247 * @return The selected OSM primitives 248 */ 249 public Collection<OsmPrimitive> getSelectedPrimitives() { 250 return model.getSelected(); 251 } 252 253 /** 254 * Updates the dialog title with a summary of the current JOSM selection 255 */ 256 class TitleUpdater implements ListDataListener { 257 protected void updateTitle() { 258 setTitle(model.getJOSMSelectionSummary()); 259 } 260 261 @Override 262 public void contentsChanged(ListDataEvent e) { 263 updateTitle(); 264 } 265 266 @Override 267 public void intervalAdded(ListDataEvent e) { 268 updateTitle(); 269 } 270 271 @Override 272 public void intervalRemoved(ListDataEvent e) { 273 updateTitle(); 274 } 275 } 276 277 /** 278 * Launches the search dialog 279 */ 280 static class SearchAction extends AbstractAction implements ActiveLayerChangeListener { 281 /** 282 * Constructs a new {@code SearchAction}. 283 */ 284 SearchAction() { 285 putValue(NAME, tr("Search")); 286 putValue(SHORT_DESCRIPTION, tr("Search for objects")); 287 new ImageProvider("dialogs", "search").getResource().attachImageIcon(this, true); 288 updateEnabledState(); 289 } 290 291 @Override 292 public void actionPerformed(ActionEvent e) { 293 if (!isEnabled()) return; 294 org.openstreetmap.josm.actions.search.SearchAction.search(); 295 } 296 297 protected void updateEnabledState() { 298 setEnabled(Main.getLayerManager().getEditLayer() != null); 299 } 300 301 @Override 302 public void activeOrEditLayerChanged(ActiveLayerChangeEvent e) { 303 updateEnabledState(); 304 } 305 } 306 307 /** 308 * Sets the current JOSM selection to the OSM primitives selected in the list 309 * of this dialog 310 */ 311 class SelectAction extends AbstractSelectAction implements ListSelectionListener { 312 /** 313 * Constructs a new {@code SelectAction}. 314 */ 315 SelectAction() { 316 updateEnabledState(); 317 } 318 319 @Override 320 public void actionPerformed(ActionEvent e) { 321 Collection<OsmPrimitive> sel = model.getSelected(); 322 if (sel.isEmpty()) return; 323 OsmDataLayer editLayer = Main.getLayerManager().getEditLayer(); 324 if (editLayer == null) return; 325 editLayer.data.setSelected(sel); 326 model.selectionModel.setSelectionInterval(0, sel.size()-1); 327 } 328 329 protected void updateEnabledState() { 330 setEnabled(!model.isSelectionEmpty()); 331 } 332 333 @Override 334 public void valueChanged(ListSelectionEvent e) { 335 updateEnabledState(); 336 } 337 } 338 339 /** 340 * The action for showing history information of the current history item. 341 */ 342 class ShowHistoryAction extends AbstractAction implements ListSelectionListener, SelectionChangedListener { 343 /** 344 * Constructs a new {@code ShowHistoryAction}. 345 */ 346 ShowHistoryAction() { 347 putValue(NAME, tr("History")); 348 putValue(SHORT_DESCRIPTION, tr("Display the history of the selected objects.")); 349 new ImageProvider("dialogs", "history").getResource().attachImageIcon(this, true); 350 updateEnabledState(model.getSize()); 351 } 352 353 @Override 354 public void actionPerformed(ActionEvent e) { 355 Collection<OsmPrimitive> sel = model.getSelected(); 356 if (sel.isEmpty() && model.getSize() != 1) { 357 return; 358 } else if (sel.isEmpty()) { 359 sel = Collections.singleton(model.getElementAt(0)); 360 } 361 HistoryBrowserDialogManager.getInstance().showHistory(sel); 362 } 363 364 protected void updateEnabledState(int osmSelectionSize) { 365 // See #10830 - allow to click on history button is a single object is selected, even if not selected again in the list 366 setEnabled(!model.isSelectionEmpty() || osmSelectionSize == 1); 367 } 368 369 @Override 370 public void valueChanged(ListSelectionEvent e) { 371 updateEnabledState(model.getSize()); 372 } 373 374 @Override 375 public void selectionChanged(Collection<? extends OsmPrimitive> newSelection) { 376 updateEnabledState(newSelection.size()); 377 } 378 } 379 380 /** 381 * The action for zooming to the primitives in the current JOSM selection 382 * 383 */ 384 class ZoomToJOSMSelectionAction extends AbstractAction implements ListDataListener { 385 386 ZoomToJOSMSelectionAction() { 387 putValue(NAME, tr("Zoom to selection")); 388 putValue(SHORT_DESCRIPTION, tr("Zoom to selection")); 389 new ImageProvider("dialogs/autoscale", "selection").getResource().attachImageIcon(this, true); 390 updateEnabledState(); 391 } 392 393 @Override 394 public void actionPerformed(ActionEvent e) { 395 AutoScaleAction.autoScale("selection"); 396 } 397 398 public void updateEnabledState() { 399 setEnabled(model.getSize() > 0); 400 } 401 402 @Override 403 public void contentsChanged(ListDataEvent e) { 404 updateEnabledState(); 405 } 406 407 @Override 408 public void intervalAdded(ListDataEvent e) { 409 updateEnabledState(); 410 } 411 412 @Override 413 public void intervalRemoved(ListDataEvent e) { 414 updateEnabledState(); 415 } 416 } 417 418 /** 419 * The action for zooming to the primitives which are currently selected in 420 * the list displaying the JOSM selection 421 * 422 */ 423 class ZoomToListSelection extends AbstractAction implements ListSelectionListener { 424 /** 425 * Constructs a new {@code ZoomToListSelection}. 426 */ 427 ZoomToListSelection() { 428 putValue(NAME, tr("Zoom to selected element(s)")); 429 putValue(SHORT_DESCRIPTION, tr("Zoom to selected element(s)")); 430 new ImageProvider("dialogs/autoscale", "selection").getResource().attachImageIcon(this, true); 431 updateEnabledState(); 432 } 433 434 @Override 435 public void actionPerformed(ActionEvent e) { 436 BoundingXYVisitor box = new BoundingXYVisitor(); 437 Collection<OsmPrimitive> sel = model.getSelected(); 438 if (sel.isEmpty()) return; 439 box.computeBoundingBox(sel); 440 if (box.getBounds() == null) 441 return; 442 box.enlargeBoundingBox(); 443 Main.map.mapView.zoomTo(box); 444 } 445 446 protected void updateEnabledState() { 447 setEnabled(!model.isSelectionEmpty()); 448 } 449 450 @Override 451 public void valueChanged(ListSelectionEvent e) { 452 updateEnabledState(); 453 } 454 } 455 456 /** 457 * The list model for the list of OSM primitives in the current JOSM selection. 458 * 459 * The model also maintains a history of the last {@link SelectionListModel#SELECTION_HISTORY_SIZE} 460 * JOSM selection. 461 * 462 */ 463 private static class SelectionListModel extends AbstractListModel<OsmPrimitive> 464 implements ActiveLayerChangeListener, SelectionChangedListener, DataSetListener { 465 466 private static final int SELECTION_HISTORY_SIZE = 10; 467 468 // Variable to store history from currentDataSet() 469 private LinkedList<Collection<? extends OsmPrimitive>> history; 470 private final transient List<OsmPrimitive> selection = new ArrayList<>(); 471 private final DefaultListSelectionModel selectionModel; 472 473 /** 474 * Constructor 475 * @param selectionModel the selection model used in the list 476 */ 477 SelectionListModel(DefaultListSelectionModel selectionModel) { 478 this.selectionModel = selectionModel; 479 } 480 481 /** 482 * Replies a summary of the current JOSM selection 483 * 484 * @return a summary of the current JOSM selection 485 */ 486 public synchronized String getJOSMSelectionSummary() { 487 if (selection.isEmpty()) return tr("Selection"); 488 int numNodes = 0; 489 int numWays = 0; 490 int numRelations = 0; 491 for (OsmPrimitive p: selection) { 492 switch(p.getType()) { 493 case NODE: numNodes++; break; 494 case WAY: numWays++; break; 495 case RELATION: numRelations++; break; 496 default: throw new AssertionError(); 497 } 498 } 499 return tr("Sel.: Rel.:{0} / Ways:{1} / Nodes:{2}", numRelations, numWays, numNodes); 500 } 501 502 /** 503 * Remembers a JOSM selection the history of JOSM selections 504 * 505 * @param selection the JOSM selection. Ignored if null or empty. 506 */ 507 public void remember(Collection<? extends OsmPrimitive> selection) { 508 if (selection == null) return; 509 if (selection.isEmpty()) return; 510 if (history == null) return; 511 if (history.isEmpty()) { 512 history.add(selection); 513 return; 514 } 515 if (history.getFirst().equals(selection)) return; 516 history.addFirst(selection); 517 for (int i = 1; i < history.size(); ++i) { 518 if (history.get(i).equals(selection)) { 519 history.remove(i); 520 break; 521 } 522 } 523 int maxsize = Main.pref.getInteger("select.history-size", SELECTION_HISTORY_SIZE); 524 while (history.size() > maxsize) { 525 history.removeLast(); 526 } 527 } 528 529 /** 530 * Replies the history of JOSM selections 531 * 532 * @return history of JOSM selections 533 */ 534 public List<Collection<? extends OsmPrimitive>> getSelectionHistory() { 535 return history; 536 } 537 538 @Override 539 public synchronized OsmPrimitive getElementAt(int index) { 540 return selection.get(index); 541 } 542 543 @Override 544 public synchronized int getSize() { 545 return selection.size(); 546 } 547 548 /** 549 * Determines if no OSM primitives are currently selected. 550 * @return {@code true} if no OSM primitives are currently selected 551 * @since 10383 552 */ 553 public boolean isSelectionEmpty() { 554 return selectionModel.isSelectionEmpty(); 555 } 556 557 /** 558 * Replies the collection of OSM primitives currently selected in the view of this model 559 * 560 * @return choosen elements in the view 561 */ 562 public synchronized Collection<OsmPrimitive> getSelected() { 563 Set<OsmPrimitive> sel = new HashSet<>(); 564 for (int i = 0; i < getSize(); i++) { 565 if (selectionModel.isSelectedIndex(i)) { 566 sel.add(selection.get(i)); 567 } 568 } 569 return sel; 570 } 571 572 /** 573 * Sets the OSM primitives to be selected in the view of this model 574 * 575 * @param sel the collection of primitives to select 576 */ 577 public synchronized void setSelected(Collection<OsmPrimitive> sel) { 578 selectionModel.setValueIsAdjusting(true); 579 selectionModel.clearSelection(); 580 if (sel != null) { 581 for (OsmPrimitive p: sel) { 582 int i = selection.indexOf(p); 583 if (i >= 0) { 584 selectionModel.addSelectionInterval(i, i); 585 } 586 } 587 } 588 selectionModel.setValueIsAdjusting(false); 589 } 590 591 @Override 592 protected void fireContentsChanged(Object source, int index0, int index1) { 593 Collection<OsmPrimitive> sel = getSelected(); 594 super.fireContentsChanged(source, index0, index1); 595 setSelected(sel); 596 } 597 598 /** 599 * Sets the collection of currently selected OSM objects 600 * 601 * @param selection the collection of currently selected OSM objects 602 */ 603 public void setJOSMSelection(final Collection<? extends OsmPrimitive> selection) { 604 synchronized (this) { 605 this.selection.clear(); 606 if (selection != null) { 607 this.selection.addAll(selection); 608 sort(); 609 } 610 } 611 GuiHelper.runInEDTAndWait(new Runnable() { 612 @Override public void run() { 613 fireContentsChanged(this, 0, getSize()); 614 if (selection != null) { 615 remember(selection); 616 if (selection.size() == 2) { 617 Iterator<? extends OsmPrimitive> it = selection.iterator(); 618 OsmPrimitive n1 = it.next(); 619 OsmPrimitive n2 = it.next(); 620 // show distance between two selected nodes with coordinates 621 if (n1 instanceof Node && n2 instanceof Node) { 622 LatLon c1 = ((Node) n1).getCoor(); 623 LatLon c2 = ((Node) n2).getCoor(); 624 if (c1 != null && c2 != null) { 625 Main.map.statusLine.setDist(c1.greatCircleDistance(c2)); 626 return; 627 } 628 } 629 } 630 Main.map.statusLine.setDist( 631 new SubclassFilteredCollection<OsmPrimitive, Way>(selection, Way.class::isInstance)); 632 } 633 } 634 }); 635 } 636 637 /** 638 * Triggers a refresh of the view for all primitives in {@code toUpdate} 639 * which are currently displayed in the view 640 * 641 * @param toUpdate the collection of primitives to update 642 */ 643 public synchronized void update(Collection<? extends OsmPrimitive> toUpdate) { 644 if (toUpdate == null) return; 645 if (toUpdate.isEmpty()) return; 646 Collection<OsmPrimitive> sel = getSelected(); 647 for (OsmPrimitive p: toUpdate) { 648 int i = selection.indexOf(p); 649 if (i >= 0) { 650 super.fireContentsChanged(this, i, i); 651 } 652 } 653 setSelected(sel); 654 } 655 656 /** 657 * Sorts the current elements in the selection 658 */ 659 public synchronized void sort() { 660 if (selection.size() <= Main.pref.getInteger("selection.no_sort_above", 100_000)) { 661 boolean quick = selection.size() > Main.pref.getInteger("selection.fast_sort_above", 10_000); 662 selection.sort(OsmPrimitiveComparator.orderingWaysRelationsNodes().thenComparing(quick 663 ? OsmPrimitiveComparator.comparingUniqueId() 664 : OsmPrimitiveComparator.comparingNames())); 665 } 666 } 667 668 /* ------------------------------------------------------------------------ */ 669 /* interface ActiveLayerChangeListener */ 670 /* ------------------------------------------------------------------------ */ 671 @Override 672 public void activeOrEditLayerChanged(ActiveLayerChangeEvent e) { 673 DataSet newData = e.getSource().getEditDataSet(); 674 if (newData == null) { 675 setJOSMSelection(null); 676 history = null; 677 } else { 678 history = newData.getSelectionHistory(); 679 setJOSMSelection(newData.getAllSelected()); 680 } 681 } 682 683 /* ------------------------------------------------------------------------ */ 684 /* interface SelectionChangeListener */ 685 /* ------------------------------------------------------------------------ */ 686 @Override 687 public void selectionChanged(Collection<? extends OsmPrimitive> newSelection) { 688 setJOSMSelection(newSelection); 689 } 690 691 /* ------------------------------------------------------------------------ */ 692 /* interface DataSetListener */ 693 /* ------------------------------------------------------------------------ */ 694 @Override 695 public void dataChanged(DataChangedEvent event) { 696 // refresh the whole list 697 fireContentsChanged(this, 0, getSize()); 698 } 699 700 @Override 701 public void nodeMoved(NodeMovedEvent event) { 702 // may influence the display name of primitives, update the data 703 update(event.getPrimitives()); 704 } 705 706 @Override 707 public void otherDatasetChange(AbstractDatasetChangedEvent event) { 708 // may influence the display name of primitives, update the data 709 update(event.getPrimitives()); 710 } 711 712 @Override 713 public void relationMembersChanged(RelationMembersChangedEvent event) { 714 // may influence the display name of primitives, update the data 715 update(event.getPrimitives()); 716 } 717 718 @Override 719 public void tagsChanged(TagsChangedEvent event) { 720 // may influence the display name of primitives, update the data 721 update(event.getPrimitives()); 722 } 723 724 @Override 725 public void wayNodesChanged(WayNodesChangedEvent event) { 726 // may influence the display name of primitives, update the data 727 update(event.getPrimitives()); 728 } 729 730 @Override 731 public void primitivesAdded(PrimitivesAddedEvent event) { 732 /* ignored - handled by SelectionChangeListener */ 733 } 734 735 @Override 736 public void primitivesRemoved(PrimitivesRemovedEvent event) { 737 /* ignored - handled by SelectionChangeListener*/ 738 } 739 } 740 741 /** 742 * A specialized {@link JMenuItem} for presenting one entry of the search history 743 * 744 * @author Jan Peter Stotz 745 */ 746 protected static class SearchMenuItem extends JMenuItem implements ActionListener { 747 protected final transient SearchSetting s; 748 749 public SearchMenuItem(SearchSetting s) { 750 super(Utils.shortenString(s.toString(), 751 org.openstreetmap.josm.actions.search.SearchAction.MAX_LENGTH_SEARCH_EXPRESSION_DISPLAY)); 752 this.s = s; 753 addActionListener(this); 754 } 755 756 @Override 757 public void actionPerformed(ActionEvent e) { 758 org.openstreetmap.josm.actions.search.SearchAction.searchWithoutHistory(s); 759 } 760 } 761 762 /** 763 * The popup menu for the search history entries 764 * 765 */ 766 protected static class SearchPopupMenu extends JPopupMenu { 767 public static void launch(Component parent) { 768 if (org.openstreetmap.josm.actions.search.SearchAction.getSearchHistory().isEmpty()) 769 return; 770 JPopupMenu menu = new SearchPopupMenu(); 771 Rectangle r = parent.getBounds(); 772 menu.show(parent, r.x, r.y + r.height); 773 } 774 775 /** 776 * Constructs a new {@code SearchPopupMenu}. 777 */ 778 public SearchPopupMenu() { 779 for (SearchSetting ss: org.openstreetmap.josm.actions.search.SearchAction.getSearchHistory()) { 780 add(new SearchMenuItem(ss)); 781 } 782 } 783 } 784 785 /** 786 * A specialized {@link JMenuItem} for presenting one entry of the selection history 787 * 788 * @author Jan Peter Stotz 789 */ 790 protected static class SelectionMenuItem extends JMenuItem implements ActionListener { 791 protected transient Collection<? extends OsmPrimitive> sel; 792 793 public SelectionMenuItem(Collection<? extends OsmPrimitive> sel) { 794 this.sel = sel; 795 int ways = 0; 796 int nodes = 0; 797 int relations = 0; 798 for (OsmPrimitive o : sel) { 799 if (!o.isSelectable()) continue; // skip unselectable primitives 800 if (o instanceof Way) { 801 ways++; 802 } else if (o instanceof Node) { 803 nodes++; 804 } else if (o instanceof Relation) { 805 relations++; 806 } 807 } 808 StringBuilder text = new StringBuilder(); 809 if (ways != 0) { 810 text.append(text.length() > 0 ? ", " : "") 811 .append(trn("{0} way", "{0} ways", ways, ways)); 812 } 813 if (nodes != 0) { 814 text.append(text.length() > 0 ? ", " : "") 815 .append(trn("{0} node", "{0} nodes", nodes, nodes)); 816 } 817 if (relations != 0) { 818 text.append(text.length() > 0 ? ", " : "") 819 .append(trn("{0} relation", "{0} relations", relations, relations)); 820 } 821 if (ways + nodes + relations == 0) { 822 text.append(tr("Unselectable now")); 823 this.sel = new ArrayList<>(); // empty selection 824 } 825 DefaultNameFormatter df = DefaultNameFormatter.getInstance(); 826 if (ways + nodes + relations == 1) { 827 text.append(": "); 828 for (OsmPrimitive o : sel) { 829 text.append(o.getDisplayName(df)); 830 } 831 setText(text.toString()); 832 } else { 833 setText(tr("Selection: {0}", text)); 834 } 835 addActionListener(this); 836 } 837 838 @Override 839 public void actionPerformed(ActionEvent e) { 840 Main.getLayerManager().getEditDataSet().setSelected(sel); 841 } 842 } 843 844 /** 845 * The popup menu for the JOSM selection history entries 846 */ 847 protected static class SelectionHistoryPopup extends JPopupMenu { 848 public static void launch(Component parent, Collection<Collection<? extends OsmPrimitive>> history) { 849 if (history == null || history.isEmpty()) return; 850 JPopupMenu menu = new SelectionHistoryPopup(history); 851 Rectangle r = parent.getBounds(); 852 menu.show(parent, r.x, r.y + r.height); 853 } 854 855 public SelectionHistoryPopup(Collection<Collection<? extends OsmPrimitive>> history) { 856 for (Collection<? extends OsmPrimitive> sel : history) { 857 add(new SelectionMenuItem(sel)); 858 } 859 } 860 } 861 862 /** 863 * A transfer handler class for drag-and-drop support. 864 */ 865 protected class SelectionTransferHandler extends TransferHandler { 866 867 @Override 868 public int getSourceActions(JComponent c) { 869 return COPY; 870 } 871 872 @Override 873 protected Transferable createTransferable(JComponent c) { 874 return new PrimitiveTransferable(PrimitiveTransferData.getData(getSelectedPrimitives())); 875 } 876 } 877}