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.gui.MapView; 046import org.openstreetmap.josm.gui.SideButton; 047import org.openstreetmap.josm.gui.dialogs.changeset.ChangesetCacheManager; 048import org.openstreetmap.josm.gui.dialogs.changeset.ChangesetInSelectionListModel; 049import org.openstreetmap.josm.gui.dialogs.changeset.ChangesetListCellRenderer; 050import org.openstreetmap.josm.gui.dialogs.changeset.ChangesetListModel; 051import org.openstreetmap.josm.gui.dialogs.changeset.ChangesetsInActiveDataLayerListModel; 052import org.openstreetmap.josm.gui.help.HelpUtil; 053import org.openstreetmap.josm.gui.io.CloseChangesetTask; 054import org.openstreetmap.josm.gui.layer.OsmDataLayer; 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.tools.ImageProvider; 060import org.openstreetmap.josm.tools.OpenBrowser; 061import org.openstreetmap.josm.tools.bugreport.BugReportExceptionHandler; 062 063/** 064 * ChangesetDialog is a toggle dialog which displays the current list of changesets. 065 * It either displays 066 * <ul> 067 * <li>the list of changesets the currently selected objects are assigned to</li> 068 * <li>the list of changesets objects in the current data layer are assigend to</li> 069 * </ul> 070 * 071 * The dialog offers actions to download and to close changesets. It can also launch an external 072 * browser with information about a changeset. Furthermore, it can select all objects in 073 * the current data layer being assigned to a specific changeset. 074 * @since 2613 075 */ 076public class ChangesetDialog extends ToggleDialog { 077 private ChangesetInSelectionListModel inSelectionModel; 078 private ChangesetsInActiveDataLayerListModel inActiveDataLayerModel; 079 private JList<Changeset> lstInSelection; 080 private JList<Changeset> lstInActiveDataLayer; 081 private JCheckBox cbInSelectionOnly; 082 private JPanel pnlList; 083 084 // the actions 085 private SelectObjectsAction selectObjectsAction; 086 private ReadChangesetsAction readChangesetAction; 087 private ShowChangesetInfoAction showChangesetInfoAction; 088 private CloseOpenChangesetsAction closeChangesetAction; 089 private LaunchChangesetManagerAction launchChangesetManagerAction; 090 091 private ChangesetDialogPopup popupMenu; 092 093 protected void buildChangesetsLists() { 094 DefaultListSelectionModel selectionModel = new DefaultListSelectionModel(); 095 inSelectionModel = new ChangesetInSelectionListModel(selectionModel); 096 097 lstInSelection = new JList<>(inSelectionModel); 098 lstInSelection.setSelectionModel(selectionModel); 099 lstInSelection.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); 100 lstInSelection.setCellRenderer(new ChangesetListCellRenderer()); 101 102 selectionModel = new DefaultListSelectionModel(); 103 inActiveDataLayerModel = new ChangesetsInActiveDataLayerListModel(selectionModel); 104 lstInActiveDataLayer = new JList<>(inActiveDataLayerModel); 105 lstInActiveDataLayer.setSelectionModel(selectionModel); 106 lstInActiveDataLayer.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); 107 lstInActiveDataLayer.setCellRenderer(new ChangesetListCellRenderer()); 108 109 DblClickHandler dblClickHandler = new DblClickHandler(); 110 lstInSelection.addMouseListener(dblClickHandler); 111 lstInActiveDataLayer.addMouseListener(dblClickHandler); 112 } 113 114 protected void registerAsListener() { 115 // let the model for changesets in the current selection listen to various events 116 ChangesetCache.getInstance().addChangesetCacheListener(inSelectionModel); 117 MapView.addEditLayerChangeListener(inSelectionModel); 118 DataSet.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 MapView.addEditLayerChangeListener(inActiveDataLayerModel); 124 OsmDataLayer editLayer = Main.main.getEditLayer(); 125 if (editLayer != null) { 126 editLayer.data.addDataSetListener(inActiveDataLayerModel); 127 inActiveDataLayerModel.initFromDataSet(editLayer.data); 128 inSelectionModel.initFromPrimitives(editLayer.data.getAllSelected()); 129 } 130 } 131 132 protected void unregisterAsListener() { 133 // remove the list model for the current edit layer as listener 134 // 135 ChangesetCache.getInstance().removeChangesetCacheListener(inActiveDataLayerModel); 136 MapView.removeEditLayerChangeListener(inActiveDataLayerModel); 137 OsmDataLayer editLayer = Main.main.getEditLayer(); 138 if (editLayer != null) { 139 editLayer.data.removeDataSetListener(inActiveDataLayerModel); 140 } 141 142 // remove the list model for the changesets in the current selection as 143 // listener 144 // 145 MapView.removeEditLayerChangeListener(inSelectionModel); 146 DataSet.removeSelectionListener(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(Main.pref.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 protected void build() { 184 JPanel pnl = new JPanel(new BorderLayout()); 185 pnl.add(buildFilterPanel(), BorderLayout.NORTH); 186 pnlList = buildListPanel(); 187 pnl.add(pnlList, BorderLayout.CENTER); 188 189 cbInSelectionOnly.addItemListener(new FilterChangeHandler()); 190 191 HelpUtil.setHelpContext(pnl, HelpUtil.ht("/Dialog/ChangesetList")); 192 193 // -- select objects action 194 selectObjectsAction = new SelectObjectsAction(); 195 cbInSelectionOnly.addItemListener(selectObjectsAction); 196 197 // -- read changesets action 198 readChangesetAction = new ReadChangesetsAction(); 199 cbInSelectionOnly.addItemListener(readChangesetAction); 200 201 // -- close changesets action 202 closeChangesetAction = new CloseOpenChangesetsAction(); 203 cbInSelectionOnly.addItemListener(closeChangesetAction); 204 205 // -- show info action 206 showChangesetInfoAction = new ShowChangesetInfoAction(); 207 cbInSelectionOnly.addItemListener(showChangesetInfoAction); 208 209 // -- launch changeset manager action 210 launchChangesetManagerAction = new LaunchChangesetManagerAction(); 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(new SideButton[] { 219 new SideButton(selectObjectsAction, false), 220 new SideButton(readChangesetAction, false), 221 new SideButton(closeChangesetAction, false), 222 new SideButton(showChangesetInfoAction, false), 223 new SideButton(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 OsmDataLayer editLayer = Main.main.getEditLayer(); 241 if (editLayer != null) { 242 inSelectionModel.initFromPrimitives(editLayer.data.getAllSelected()); 243 inActiveDataLayerModel.initFromDataSet(editLayer.data); 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 null, /* no keyboard shortcut */ 256 200, /* the preferred height */ 257 false /* don't show if there is no preference */ 258 ); 259 build(); 260 initWithCurrentData(); 261 } 262 263 class DblClickHandler extends MouseAdapter { 264 @Override 265 public void mouseClicked(MouseEvent e) { 266 if (!SwingUtilities.isLeftMouseButton(e) || e.getClickCount() < 2) 267 return; 268 Set<Integer> sel = getCurrentChangesetListModel().getSelectedChangesetIds(); 269 if (sel.isEmpty()) 270 return; 271 if (Main.main.getCurrentDataSet() == null) 272 return; 273 new SelectObjectsAction().selectObjectsByChangesetIds(Main.main.getCurrentDataSet(), sel); 274 } 275 276 } 277 278 class FilterChangeHandler implements ItemListener { 279 @Override 280 public void itemStateChanged(ItemEvent e) { 281 Main.pref.put("changeset-dialog.for-selected-objects-only", cbInSelectionOnly.isSelected()); 282 pnlList.removeAll(); 283 if (cbInSelectionOnly.isSelected()) { 284 pnlList.add(new JScrollPane(lstInSelection), BorderLayout.CENTER); 285 } else { 286 pnlList.add(new JScrollPane(lstInActiveDataLayer), BorderLayout.CENTER); 287 } 288 validate(); 289 repaint(); 290 } 291 } 292 293 /** 294 * Selects objects for the currently selected changesets. 295 */ 296 class SelectObjectsAction extends AbstractAction implements ListSelectionListener, ItemListener { 297 298 SelectObjectsAction() { 299 putValue(NAME, tr("Select")); 300 putValue(SHORT_DESCRIPTION, tr("Select all objects assigned to the currently selected changesets")); 301 putValue(SMALL_ICON, ImageProvider.get("dialogs", "select")); 302 updateEnabledState(); 303 } 304 305 public void selectObjectsByChangesetIds(DataSet ds, Set<Integer> ids) { 306 if (ds == null || ids == null) 307 return; 308 Set<OsmPrimitive> sel = new HashSet<>(); 309 for (OsmPrimitive p: ds.allPrimitives()) { 310 if (ids.contains(p.getChangesetId())) { 311 sel.add(p); 312 } 313 } 314 ds.setSelected(sel); 315 } 316 317 @Override 318 public void actionPerformed(ActionEvent e) { 319 if (!Main.main.hasEditLayer()) 320 return; 321 ChangesetListModel model = getCurrentChangesetListModel(); 322 Set<Integer> sel = model.getSelectedChangesetIds(); 323 if (sel.isEmpty()) 324 return; 325 326 DataSet ds = Main.main.getEditLayer().data; 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 putValue(SMALL_ICON, ImageProvider.get("download")); 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 Main.worker.submit(new PostDownloadHandler(task, task.download())); 366 } 367 368 protected void updateEnabledState() { 369 setEnabled(getCurrentChangesetList().getSelectedIndices().length > 0 && !Main.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("Closes the selected open changesets")); 391 putValue(SMALL_ICON, ImageProvider.get("closechangeset")); 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 Main.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 putValue(SMALL_ICON, ImageProvider.get("help/internet")); 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 && !AbstractInfoAction.confirmLaunchMultiple(sel.size())) 436 return; 437 String baseUrl = Main.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 putValue(SMALL_ICON, ImageProvider.get("dialogs/changeset", "changesetmanager")); 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 protected static void launchChangesetManager(Collection<Integer> toSelect) { 487 ChangesetCacheManager cm = ChangesetCacheManager.getInstance(); 488 if (cm.isVisible()) { 489 cm.setExtendedState(Frame.NORMAL); 490 cm.toFront(); 491 cm.requestFocus(); 492 } else { 493 cm.setVisible(true); 494 cm.toFront(); 495 cm.requestFocus(); 496 } 497 cm.setSelectedChangesetsById(toSelect); 498 } 499 500 /** 501 * Fetches changesets and display the changeset dialog. 502 * @param sel the changeset ids to fetch and display. 503 */ 504 public static void displayChangesets(final Set<Integer> sel) { 505 final Set<Integer> toDownload = new HashSet<>(); 506 if (!Main.isOffline(OnlineResource.OSM_API)) { 507 ChangesetCache cc = ChangesetCache.getInstance(); 508 for (int id: sel) { 509 if (!cc.contains(id)) { 510 toDownload.add(id); 511 } 512 } 513 } 514 515 final ChangesetHeaderDownloadTask task; 516 final Future<?> future; 517 if (toDownload.isEmpty()) { 518 task = null; 519 future = null; 520 } else { 521 task = new ChangesetHeaderDownloadTask(toDownload); 522 future = Main.worker.submit(new PostDownloadHandler(task, task.download())); 523 } 524 525 Runnable r = new Runnable() { 526 @Override 527 public void run() { 528 // first, wait for the download task to finish, if a download task was launched 529 if (future != null) { 530 try { 531 future.get(); 532 } catch (InterruptedException e) { 533 Main.warn("InterruptedException in "+getClass().getSimpleName()+" while downloading changeset header"); 534 } catch (ExecutionException e) { 535 Main.error(e); 536 BugReportExceptionHandler.handleException(e.getCause()); 537 return; 538 } 539 } 540 if (task != null) { 541 if (task.isCanceled()) 542 // don't launch the changeset manager if the download task was canceled 543 return; 544 if (task.isFailed()) { 545 toDownload.clear(); 546 } 547 } 548 // launch the task 549 GuiHelper.runInEDT(new Runnable() { 550 @Override 551 public void run() { 552 launchChangesetManager(sel); 553 } 554 }); 555 } 556 }; 557 Main.worker.submit(r); 558 } 559 } 560 561 class ChangesetDialogPopup extends ListPopupMenu { 562 ChangesetDialogPopup(JList<?> ... lists) { 563 super(lists); 564 add(selectObjectsAction); 565 addSeparator(); 566 add(readChangesetAction); 567 add(closeChangesetAction); 568 addSeparator(); 569 add(showChangesetInfoAction); 570 } 571 } 572 573 public void addPopupMenuSeparator() { 574 popupMenu.addSeparator(); 575 } 576 577 public JMenuItem addPopupMenuAction(Action a) { 578 return popupMenu.add(a); 579 } 580}