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