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