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