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