001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.gui.dialogs; 003 004import static org.openstreetmap.josm.tools.I18n.tr; 005 006import java.awt.BorderLayout; 007import java.awt.FlowLayout; 008import java.awt.Frame; 009import java.awt.event.ActionEvent; 010import java.awt.event.ItemEvent; 011import java.awt.event.ItemListener; 012import java.awt.event.MouseAdapter; 013import java.awt.event.MouseEvent; 014import java.awt.event.KeyEvent; 015import java.util.Arrays; 016import java.util.Collection; 017import java.util.HashSet; 018import java.util.List; 019import java.util.Set; 020import java.util.concurrent.ExecutionException; 021import java.util.concurrent.Future; 022import java.util.stream.Collectors; 023 024import javax.swing.AbstractAction; 025import javax.swing.Action; 026import javax.swing.DefaultListSelectionModel; 027import javax.swing.JCheckBox; 028import javax.swing.JList; 029import javax.swing.JMenuItem; 030import javax.swing.JPanel; 031import javax.swing.JScrollPane; 032import javax.swing.ListSelectionModel; 033import javax.swing.SwingUtilities; 034import javax.swing.event.ListSelectionEvent; 035import javax.swing.event.ListSelectionListener; 036 037import org.openstreetmap.josm.actions.OpenBrowserAction; 038import org.openstreetmap.josm.actions.downloadtasks.ChangesetHeaderDownloadTask; 039import org.openstreetmap.josm.actions.downloadtasks.PostDownloadHandler; 040import org.openstreetmap.josm.data.osm.Changeset; 041import org.openstreetmap.josm.data.osm.ChangesetCache; 042import org.openstreetmap.josm.data.osm.DataSet; 043import org.openstreetmap.josm.data.osm.OsmPrimitive; 044import org.openstreetmap.josm.data.osm.event.DatasetEventManager; 045import org.openstreetmap.josm.data.osm.event.DatasetEventManager.FireMode; 046import org.openstreetmap.josm.data.osm.event.SelectionEventManager; 047import org.openstreetmap.josm.gui.MainApplication; 048import org.openstreetmap.josm.gui.SideButton; 049import org.openstreetmap.josm.gui.dialogs.changeset.ChangesetCacheManager; 050import org.openstreetmap.josm.gui.dialogs.changeset.ChangesetInSelectionListModel; 051import org.openstreetmap.josm.gui.dialogs.changeset.ChangesetListCellRenderer; 052import org.openstreetmap.josm.gui.dialogs.changeset.ChangesetListModel; 053import org.openstreetmap.josm.gui.dialogs.changeset.ChangesetsInActiveDataLayerListModel; 054import org.openstreetmap.josm.gui.help.HelpUtil; 055import org.openstreetmap.josm.gui.io.CloseChangesetTask; 056import org.openstreetmap.josm.gui.util.GuiHelper; 057import org.openstreetmap.josm.gui.widgets.ListPopupMenu; 058import org.openstreetmap.josm.gui.widgets.PopupMenuLauncher; 059import org.openstreetmap.josm.io.NetworkManager; 060import org.openstreetmap.josm.io.OnlineResource; 061import org.openstreetmap.josm.spi.preferences.Config; 062import org.openstreetmap.josm.tools.ImageProvider; 063import org.openstreetmap.josm.tools.Logging; 064import org.openstreetmap.josm.tools.OpenBrowser; 065import org.openstreetmap.josm.tools.bugreport.BugReportExceptionHandler; 066import org.openstreetmap.josm.tools.Shortcut; 067 068/** 069 * ChangesetDialog is a toggle dialog which displays the current list of changesets. 070 * It either displays 071 * <ul> 072 * <li>the list of changesets the currently selected objects are assigned to</li> 073 * <li>the list of changesets objects in the current data layer are assigned to</li> 074 * </ul> 075 * 076 * The dialog offers actions to download and to close changesets. It can also launch an external 077 * browser with information about a changeset. Furthermore, it can select all objects in 078 * the current data layer being assigned to a specific changeset. 079 * @since 2613 080 */ 081public class ChangesetDialog extends ToggleDialog { 082 private ChangesetInSelectionListModel inSelectionModel; 083 private ChangesetsInActiveDataLayerListModel inActiveDataLayerModel; 084 private JList<Changeset> lstInSelection; 085 private JList<Changeset> lstInActiveDataLayer; 086 private JCheckBox cbInSelectionOnly; 087 private JPanel pnlList; 088 089 // the actions 090 private SelectObjectsAction selectObjectsAction; 091 private ReadChangesetsAction readChangesetAction; 092 private ShowChangesetInfoAction showChangesetInfoAction; 093 private CloseOpenChangesetsAction closeChangesetAction; 094 095 private ChangesetDialogPopup popupMenu; 096 097 protected void buildChangesetsLists() { 098 DefaultListSelectionModel selectionModel = new DefaultListSelectionModel(); 099 inSelectionModel = new ChangesetInSelectionListModel(selectionModel); 100 101 lstInSelection = new JList<>(inSelectionModel); 102 lstInSelection.setSelectionModel(selectionModel); 103 lstInSelection.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); 104 lstInSelection.setCellRenderer(new ChangesetListCellRenderer()); 105 106 selectionModel = new DefaultListSelectionModel(); 107 inActiveDataLayerModel = new ChangesetsInActiveDataLayerListModel(selectionModel); 108 lstInActiveDataLayer = new JList<>(inActiveDataLayerModel); 109 lstInActiveDataLayer.setSelectionModel(selectionModel); 110 lstInActiveDataLayer.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); 111 lstInActiveDataLayer.setCellRenderer(new ChangesetListCellRenderer()); 112 113 DblClickHandler dblClickHandler = new DblClickHandler(); 114 lstInSelection.addMouseListener(dblClickHandler); 115 lstInActiveDataLayer.addMouseListener(dblClickHandler); 116 } 117 118 protected void registerAsListener() { 119 // let the model for changesets in the current selection listen to various events 120 ChangesetCache.getInstance().addChangesetCacheListener(inSelectionModel); 121 SelectionEventManager.getInstance().addSelectionListener(inSelectionModel); 122 123 // let the model for changesets in the current layer listen to various 124 // events and bootstrap it's content 125 ChangesetCache.getInstance().addChangesetCacheListener(inActiveDataLayerModel); 126 MainApplication.getLayerManager().addActiveLayerChangeListener(inActiveDataLayerModel); 127 DataSet ds = MainApplication.getLayerManager().getActiveDataSet(); 128 if (ds != null) { 129 ds.addDataSetListener(inActiveDataLayerModel); 130 inActiveDataLayerModel.initFromDataSet(ds); 131 inSelectionModel.initFromPrimitives(ds.getAllSelected()); 132 } 133 } 134 135 protected void unregisterAsListener() { 136 // remove the list model for the current edit layer as listener 137 ChangesetCache.getInstance().removeChangesetCacheListener(inActiveDataLayerModel); 138 MainApplication.getLayerManager().removeActiveLayerChangeListener(inActiveDataLayerModel); 139 DataSet ds = MainApplication.getLayerManager().getActiveDataSet(); 140 if (ds != null) { 141 ds.removeDataSetListener(inActiveDataLayerModel); 142 } 143 144 // remove the list model for the changesets in the current selection as listener 145 SelectionEventManager.getInstance().removeSelectionListener(inSelectionModel); 146 ChangesetCache.getInstance().removeChangesetCacheListener(inSelectionModel); 147 } 148 149 @Override 150 public void showNotify() { 151 registerAsListener(); 152 DatasetEventManager.getInstance().addDatasetListener(inActiveDataLayerModel, FireMode.IN_EDT); 153 } 154 155 @Override 156 public void hideNotify() { 157 unregisterAsListener(); 158 DatasetEventManager.getInstance().removeDatasetListener(inActiveDataLayerModel); 159 } 160 161 protected JPanel buildFilterPanel() { 162 JPanel pnl = new JPanel(new FlowLayout(FlowLayout.LEFT)); 163 pnl.setBorder(null); 164 cbInSelectionOnly = new JCheckBox(tr("For selected objects only")); 165 pnl.add(cbInSelectionOnly); 166 cbInSelectionOnly.setToolTipText(tr("<html>Select to show changesets for the currently selected objects only.<br>" 167 + "Unselect to show all changesets for objects in the current data layer.</html>")); 168 cbInSelectionOnly.setSelected(Config.getPref().getBoolean("changeset-dialog.for-selected-objects-only", false)); 169 return pnl; 170 } 171 172 protected JPanel buildListPanel() { 173 buildChangesetsLists(); 174 JPanel pnl = new JPanel(new BorderLayout()); 175 if (cbInSelectionOnly.isSelected()) { 176 pnl.add(new JScrollPane(lstInSelection)); 177 } else { 178 pnl.add(new JScrollPane(lstInActiveDataLayer)); 179 } 180 return pnl; 181 } 182 183 @Override 184 public String helpTopic() { 185 return HelpUtil.ht("/Dialog/ChangesetList"); 186 } 187 188 protected void build() { 189 JPanel pnl = new JPanel(new BorderLayout()); 190 pnl.add(buildFilterPanel(), BorderLayout.NORTH); 191 pnlList = buildListPanel(); 192 pnl.add(pnlList, BorderLayout.CENTER); 193 194 cbInSelectionOnly.addItemListener(new FilterChangeHandler()); 195 196 // -- select objects action 197 selectObjectsAction = new SelectObjectsAction(); 198 cbInSelectionOnly.addItemListener(selectObjectsAction); 199 200 // -- read changesets action 201 readChangesetAction = new ReadChangesetsAction(); 202 cbInSelectionOnly.addItemListener(readChangesetAction); 203 204 // -- close changesets action 205 closeChangesetAction = new CloseOpenChangesetsAction(); 206 cbInSelectionOnly.addItemListener(closeChangesetAction); 207 208 // -- show info action 209 showChangesetInfoAction = new ShowChangesetInfoAction(); 210 cbInSelectionOnly.addItemListener(showChangesetInfoAction); 211 212 popupMenu = new ChangesetDialogPopup(lstInActiveDataLayer, lstInSelection); 213 214 PopupMenuLauncher popupMenuLauncher = new PopupMenuLauncher(popupMenu); 215 lstInSelection.addMouseListener(popupMenuLauncher); 216 lstInActiveDataLayer.addMouseListener(popupMenuLauncher); 217 218 createLayout(pnl, false, Arrays.asList( 219 new SideButton(selectObjectsAction, false), 220 new SideButton(readChangesetAction, false), 221 new SideButton(closeChangesetAction, false), 222 new SideButton(showChangesetInfoAction, false), 223 new SideButton(new LaunchChangesetManagerAction(), false) 224 )); 225 } 226 227 protected JList<Changeset> getCurrentChangesetList() { 228 if (cbInSelectionOnly.isSelected()) 229 return lstInSelection; 230 return lstInActiveDataLayer; 231 } 232 233 protected ChangesetListModel getCurrentChangesetListModel() { 234 if (cbInSelectionOnly.isSelected()) 235 return inSelectionModel; 236 return inActiveDataLayerModel; 237 } 238 239 protected void initWithCurrentData() { 240 DataSet ds = MainApplication.getLayerManager().getActiveDataSet(); 241 if (ds != null) { 242 inSelectionModel.initFromPrimitives(ds.getAllSelected()); 243 inActiveDataLayerModel.initFromDataSet(ds); 244 } 245 } 246 247 /** 248 * Constructs a new {@code ChangesetDialog}. 249 */ 250 public ChangesetDialog() { 251 super( 252 tr("Changesets"), 253 "changesetdialog", 254 tr("Open the list of changesets in the current layer."), 255 Shortcut.registerShortcut("subwindow:changesets", tr("Windows: {0}", tr("Changesets")), 256 KeyEvent.CHAR_UNDEFINED, Shortcut.NONE), 257 200, /* the preferred height */ 258 false, /* don't show if there is no preference */ 259 null /* no preferences settings */, 260 true /* expert only */ 261 ); 262 build(); 263 initWithCurrentData(); 264 } 265 266 class DblClickHandler extends MouseAdapter { 267 @Override 268 public void mouseClicked(MouseEvent e) { 269 if (!SwingUtilities.isLeftMouseButton(e) || e.getClickCount() < 2) 270 return; 271 Set<Integer> sel = getCurrentChangesetListModel().getSelectedChangesetIds(); 272 if (sel.isEmpty()) 273 return; 274 if (MainApplication.getLayerManager().getActiveDataSet() == null) 275 return; 276 new SelectObjectsAction().selectObjectsByChangesetIds(MainApplication.getLayerManager().getActiveDataSet(), sel); 277 } 278 279 } 280 281 class FilterChangeHandler implements ItemListener { 282 @Override 283 public void itemStateChanged(ItemEvent e) { 284 Config.getPref().putBoolean("changeset-dialog.for-selected-objects-only", cbInSelectionOnly.isSelected()); 285 pnlList.removeAll(); 286 if (cbInSelectionOnly.isSelected()) { 287 pnlList.add(new JScrollPane(lstInSelection), BorderLayout.CENTER); 288 } else { 289 pnlList.add(new JScrollPane(lstInActiveDataLayer), BorderLayout.CENTER); 290 } 291 validate(); 292 repaint(); 293 } 294 } 295 296 /** 297 * Selects objects for the currently selected changesets. 298 */ 299 class SelectObjectsAction extends AbstractAction implements ListSelectionListener, ItemListener { 300 301 SelectObjectsAction() { 302 putValue(NAME, tr("Select")); 303 putValue(SHORT_DESCRIPTION, tr("Select all objects assigned to the currently selected changesets")); 304 new ImageProvider("dialogs", "select").getResource().attachImageIcon(this, true); 305 updateEnabledState(); 306 } 307 308 public void selectObjectsByChangesetIds(DataSet ds, Set<Integer> ids) { 309 if (ds == null || ids == null) 310 return; 311 Set<OsmPrimitive> sel = ds.allPrimitives().stream() 312 .filter(p -> ids.contains(p.getChangesetId())) 313 .collect(Collectors.toSet()); 314 ds.setSelected(sel); 315 } 316 317 @Override 318 public void actionPerformed(ActionEvent e) { 319 DataSet ds = MainApplication.getLayerManager().getActiveDataSet(); 320 if (ds == null) 321 return; 322 ChangesetListModel model = getCurrentChangesetListModel(); 323 Set<Integer> sel = model.getSelectedChangesetIds(); 324 if (sel.isEmpty()) 325 return; 326 327 selectObjectsByChangesetIds(ds, sel); 328 } 329 330 protected void updateEnabledState() { 331 setEnabled(getCurrentChangesetList().getSelectedIndices().length > 0); 332 } 333 334 @Override 335 public void itemStateChanged(ItemEvent e) { 336 updateEnabledState(); 337 338 } 339 340 @Override 341 public void valueChanged(ListSelectionEvent e) { 342 updateEnabledState(); 343 } 344 } 345 346 /** 347 * Downloads selected changesets 348 * 349 */ 350 class ReadChangesetsAction extends AbstractAction implements ListSelectionListener, ItemListener { 351 ReadChangesetsAction() { 352 putValue(NAME, tr("Download")); 353 putValue(SHORT_DESCRIPTION, tr("Download information about the selected changesets from the OSM server")); 354 new ImageProvider("download").getResource().attachImageIcon(this, true); 355 updateEnabledState(); 356 } 357 358 @Override 359 public void actionPerformed(ActionEvent e) { 360 ChangesetListModel model = getCurrentChangesetListModel(); 361 Set<Integer> sel = model.getSelectedChangesetIds(); 362 if (sel.isEmpty()) 363 return; 364 ChangesetHeaderDownloadTask task = new ChangesetHeaderDownloadTask(sel); 365 MainApplication.worker.submit(new PostDownloadHandler(task, task.download())); 366 } 367 368 protected void updateEnabledState() { 369 setEnabled(getCurrentChangesetList().getSelectedIndices().length > 0 && !NetworkManager.isOffline(OnlineResource.OSM_API)); 370 } 371 372 @Override 373 public void itemStateChanged(ItemEvent e) { 374 updateEnabledState(); 375 } 376 377 @Override 378 public void valueChanged(ListSelectionEvent e) { 379 updateEnabledState(); 380 } 381 } 382 383 /** 384 * Closes the currently selected changesets 385 * 386 */ 387 class CloseOpenChangesetsAction extends AbstractAction implements ListSelectionListener, ItemListener { 388 CloseOpenChangesetsAction() { 389 putValue(NAME, tr("Close open changesets")); 390 putValue(SHORT_DESCRIPTION, tr("Close the selected open changesets")); 391 new ImageProvider("closechangeset").getResource().attachImageIcon(this, true); 392 updateEnabledState(); 393 } 394 395 @Override 396 public void actionPerformed(ActionEvent e) { 397 List<Changeset> sel = getCurrentChangesetListModel().getSelectedOpenChangesets(); 398 if (sel.isEmpty()) 399 return; 400 MainApplication.worker.submit(new CloseChangesetTask(sel)); 401 } 402 403 protected void updateEnabledState() { 404 setEnabled(getCurrentChangesetListModel().hasSelectedOpenChangesets()); 405 } 406 407 @Override 408 public void itemStateChanged(ItemEvent e) { 409 updateEnabledState(); 410 } 411 412 @Override 413 public void valueChanged(ListSelectionEvent e) { 414 updateEnabledState(); 415 } 416 } 417 418 /** 419 * Show information about the currently selected changesets 420 * 421 */ 422 class ShowChangesetInfoAction extends AbstractAction implements ListSelectionListener, ItemListener { 423 ShowChangesetInfoAction() { 424 putValue(NAME, tr("Show info")); 425 putValue(SHORT_DESCRIPTION, tr("Open a web page for each selected changeset")); 426 new ImageProvider("help/internet").getResource().attachImageIcon(this, true); 427 updateEnabledState(); 428 } 429 430 @Override 431 public void actionPerformed(ActionEvent e) { 432 Set<Changeset> sel = getCurrentChangesetListModel().getSelectedChangesets(); 433 if (sel.isEmpty()) 434 return; 435 if (sel.size() > 10 && !OpenBrowserAction.confirmLaunchMultiple(sel.size())) 436 return; 437 String baseUrl = Config.getUrls().getBaseBrowseUrl(); 438 for (Changeset cs: sel) { 439 OpenBrowser.displayUrl(baseUrl + "/changeset/" + cs.getId()); 440 } 441 } 442 443 protected void updateEnabledState() { 444 setEnabled(getCurrentChangesetList().getSelectedIndices().length > 0); 445 } 446 447 @Override 448 public void itemStateChanged(ItemEvent e) { 449 updateEnabledState(); 450 } 451 452 @Override 453 public void valueChanged(ListSelectionEvent e) { 454 updateEnabledState(); 455 } 456 } 457 458 /** 459 * Show information about the currently selected changesets 460 * 461 */ 462 class LaunchChangesetManagerAction extends AbstractAction { 463 LaunchChangesetManagerAction() { 464 putValue(NAME, tr("Details")); 465 putValue(SHORT_DESCRIPTION, tr("Opens the Changeset Manager window for the selected changesets")); 466 new ImageProvider("dialogs/changeset", "changesetmanager").getResource().attachImageIcon(this, true); 467 } 468 469 @Override 470 public void actionPerformed(ActionEvent e) { 471 ChangesetListModel model = getCurrentChangesetListModel(); 472 Set<Integer> sel = model.getSelectedChangesetIds(); 473 LaunchChangesetManager.displayChangesets(sel); 474 } 475 } 476 477 /** 478 * A utility class to fetch changesets and display the changeset dialog. 479 */ 480 public static final class LaunchChangesetManager { 481 482 private LaunchChangesetManager() { 483 // Hide implicit public constructor for utility classes 484 } 485 486 private static void launchChangesetManager(Collection<Integer> toSelect) { 487 ChangesetCacheManager cm = ChangesetCacheManager.getInstance(); 488 if (cm.isVisible()) { 489 cm.setExtendedState(Frame.NORMAL); 490 } else { 491 cm.setVisible(true); 492 } 493 cm.toFront(); 494 cm.setSelectedChangesetsById(toSelect); 495 } 496 497 /** 498 * Fetches changesets and display the changeset dialog. 499 * @param sel the changeset ids to fetch and display. 500 */ 501 public static void displayChangesets(final Set<Integer> sel) { 502 final Set<Integer> toDownload = new HashSet<>(); 503 if (!NetworkManager.isOffline(OnlineResource.OSM_API)) { 504 ChangesetCache cc = ChangesetCache.getInstance(); 505 for (int id: sel) { 506 if (!cc.contains(id)) { 507 toDownload.add(id); 508 } 509 } 510 } 511 512 final ChangesetHeaderDownloadTask task; 513 final Future<?> future; 514 if (toDownload.isEmpty()) { 515 task = null; 516 future = null; 517 } else { 518 task = new ChangesetHeaderDownloadTask(toDownload); 519 future = MainApplication.worker.submit(new PostDownloadHandler(task, task.download())); 520 } 521 522 Runnable r = () -> { 523 // first, wait for the download task to finish, if a download task was launched 524 if (future != null) { 525 try { 526 future.get(); 527 } catch (InterruptedException e1) { 528 Logging.log(Logging.LEVEL_WARN, "InterruptedException in ChangesetDialog while downloading changeset header", e1); 529 Thread.currentThread().interrupt(); 530 } catch (ExecutionException e2) { 531 Logging.error(e2); 532 BugReportExceptionHandler.handleException(e2.getCause()); 533 return; 534 } 535 } 536 if (task != null) { 537 if (task.isCanceled()) 538 // don't launch the changeset manager if the download task was canceled 539 return; 540 if (task.isFailed()) { 541 toDownload.clear(); 542 } 543 } 544 // launch the task 545 GuiHelper.runInEDT(() -> launchChangesetManager(sel)); 546 }; 547 MainApplication.worker.submit(r); 548 } 549 } 550 551 class ChangesetDialogPopup extends ListPopupMenu { 552 ChangesetDialogPopup(JList<?>... lists) { 553 super(lists); 554 add(selectObjectsAction); 555 addSeparator(); 556 add(readChangesetAction); 557 add(closeChangesetAction); 558 addSeparator(); 559 add(showChangesetInfoAction); 560 } 561 } 562 563 /** 564 * Add a separator to the popup menu 565 */ 566 public void addPopupMenuSeparator() { 567 popupMenu.addSeparator(); 568 } 569 570 /** 571 * Add a menu item to the popup menu 572 * @param a The action to add 573 * @return The menu item that was added. 574 */ 575 public JMenuItem addPopupMenuAction(Action a) { 576 return popupMenu.add(a); 577 } 578}