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