001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.gui.dialogs.changeset; 003 004import static org.openstreetmap.josm.tools.I18n.tr; 005 006import java.awt.BorderLayout; 007import java.awt.Container; 008import java.awt.Dimension; 009import java.awt.FlowLayout; 010import java.awt.event.ActionEvent; 011import java.awt.event.KeyEvent; 012import java.awt.event.MouseEvent; 013import java.awt.event.WindowAdapter; 014import java.awt.event.WindowEvent; 015import java.util.Collection; 016import java.util.HashSet; 017import java.util.List; 018import java.util.Set; 019 020import javax.swing.AbstractAction; 021import javax.swing.DefaultListSelectionModel; 022import javax.swing.JComponent; 023import javax.swing.JFrame; 024import javax.swing.JOptionPane; 025import javax.swing.JPanel; 026import javax.swing.JPopupMenu; 027import javax.swing.JScrollPane; 028import javax.swing.JSplitPane; 029import javax.swing.JTabbedPane; 030import javax.swing.JTable; 031import javax.swing.JToolBar; 032import javax.swing.KeyStroke; 033import javax.swing.ListSelectionModel; 034import javax.swing.event.ListSelectionEvent; 035import javax.swing.event.ListSelectionListener; 036 037import org.openstreetmap.josm.Main; 038import org.openstreetmap.josm.data.osm.Changeset; 039import org.openstreetmap.josm.data.osm.ChangesetCache; 040import org.openstreetmap.josm.gui.HelpAwareOptionPane; 041import org.openstreetmap.josm.gui.JosmUserIdentityManager; 042import org.openstreetmap.josm.gui.SideButton; 043import org.openstreetmap.josm.gui.dialogs.changeset.query.ChangesetQueryDialog; 044import org.openstreetmap.josm.gui.dialogs.changeset.query.ChangesetQueryTask; 045import org.openstreetmap.josm.gui.help.ContextSensitiveHelpAction; 046import org.openstreetmap.josm.gui.help.HelpUtil; 047import org.openstreetmap.josm.gui.io.CloseChangesetTask; 048import org.openstreetmap.josm.gui.widgets.PopupMenuLauncher; 049import org.openstreetmap.josm.io.ChangesetQuery; 050import org.openstreetmap.josm.tools.ImageProvider; 051import org.openstreetmap.josm.tools.WindowGeometry; 052 053/** 054 * ChangesetCacheManager manages the local cache of changesets 055 * retrieved from the OSM API. It displays both a table of the locally cached changesets 056 * and detail information about an individual changeset. It also provides actions for 057 * downloading, querying, closing changesets, in addition to removing changesets from 058 * the local cache. 059 * 060 */ 061public class ChangesetCacheManager extends JFrame { 062 063 /** the unique instance of the cache manager */ 064 private static ChangesetCacheManager instance; 065 066 /** 067 * Replies the unique instance of the changeset cache manager 068 * 069 * @return the unique instance of the changeset cache manager 070 */ 071 public static ChangesetCacheManager getInstance() { 072 if (instance == null) { 073 instance = new ChangesetCacheManager(); 074 } 075 return instance; 076 } 077 078 /** 079 * Hides and destroys the unique instance of the changeset cache 080 * manager. 081 * 082 */ 083 public static void destroyInstance() { 084 if (instance != null) { 085 instance.setVisible(true); 086 instance.dispose(); 087 instance = null; 088 } 089 } 090 091 private ChangesetCacheManagerModel model; 092 private JSplitPane spContent; 093 private boolean needsSplitPaneAdjustment; 094 095 private RemoveFromCacheAction actRemoveFromCacheAction; 096 private CloseSelectedChangesetsAction actCloseSelectedChangesetsAction; 097 private DownloadSelectedChangesetsAction actDownloadSelectedChangesets; 098 private DownloadSelectedChangesetContentAction actDownloadSelectedContent; 099 private JTable tblChangesets; 100 101 /** 102 * Creates the various models required 103 */ 104 protected void buildModel() { 105 DefaultListSelectionModel selectionModel = new DefaultListSelectionModel(); 106 selectionModel.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); 107 model = new ChangesetCacheManagerModel(selectionModel); 108 109 actRemoveFromCacheAction = new RemoveFromCacheAction(); 110 actCloseSelectedChangesetsAction = new CloseSelectedChangesetsAction(); 111 actDownloadSelectedChangesets = new DownloadSelectedChangesetsAction(); 112 actDownloadSelectedContent = new DownloadSelectedChangesetContentAction(); 113 } 114 115 /** 116 * builds the toolbar panel in the heading of the dialog 117 * 118 * @return the toolbar panel 119 */ 120 protected JPanel buildToolbarPanel() { 121 JPanel pnl = new JPanel(new FlowLayout(FlowLayout.LEFT)); 122 123 SideButton btn = new SideButton(new QueryAction()); 124 pnl.add(btn); 125 pnl.add(new SingleChangesetDownloadPanel()); 126 pnl.add(new SideButton(new DownloadMyChangesets())); 127 128 return pnl; 129 } 130 131 /** 132 * builds the button panel in the footer of the dialog 133 * 134 * @return the button row pane 135 */ 136 protected JPanel buildButtonPanel() { 137 JPanel pnl = new JPanel(new FlowLayout(FlowLayout.CENTER)); 138 139 //-- cancel and close action 140 pnl.add(new SideButton(new CancelAction())); 141 142 //-- help action 143 pnl.add(new SideButton( 144 new ContextSensitiveHelpAction( 145 HelpUtil.ht("/Dialog/ChangesetCacheManager")) 146 ) 147 ); 148 149 return pnl; 150 } 151 152 /** 153 * Builds the panel with the changeset details 154 * 155 * @return the panel with the changeset details 156 */ 157 protected JPanel buildChangesetDetailPanel() { 158 JPanel pnl = new JPanel(new BorderLayout()); 159 JTabbedPane tp = new JTabbedPane(); 160 161 // -- add the details panel 162 ChangesetDetailPanel pnlChangesetDetail; 163 tp.add(pnlChangesetDetail = new ChangesetDetailPanel()); 164 model.addPropertyChangeListener(pnlChangesetDetail); 165 166 // -- add the tags panel 167 ChangesetTagsPanel pnlChangesetTags = new ChangesetTagsPanel(); 168 tp.add(pnlChangesetTags); 169 model.addPropertyChangeListener(pnlChangesetTags); 170 171 // -- add the panel for the changeset content 172 ChangesetContentPanel pnlChangesetContent = new ChangesetContentPanel(); 173 tp.add(pnlChangesetContent); 174 model.addPropertyChangeListener(pnlChangesetContent); 175 176 tp.setTitleAt(0, tr("Properties")); 177 tp.setToolTipTextAt(0, tr("Display the basic properties of the changeset")); 178 tp.setTitleAt(1, tr("Tags")); 179 tp.setToolTipTextAt(1, tr("Display the tags of the changeset")); 180 tp.setTitleAt(2, tr("Content")); 181 tp.setToolTipTextAt(2, tr("Display the objects created, updated, and deleted by the changeset")); 182 183 pnl.add(tp, BorderLayout.CENTER); 184 return pnl; 185 } 186 187 /** 188 * builds the content panel of the dialog 189 * 190 * @return the content panel 191 */ 192 protected JPanel buildContentPanel() { 193 JPanel pnl = new JPanel(new BorderLayout()); 194 195 spContent = new JSplitPane(JSplitPane.VERTICAL_SPLIT); 196 spContent.setLeftComponent(buildChangesetTablePanel()); 197 spContent.setRightComponent(buildChangesetDetailPanel()); 198 spContent.setOneTouchExpandable(true); 199 spContent.setDividerLocation(0.5); 200 201 pnl.add(spContent, BorderLayout.CENTER); 202 return pnl; 203 } 204 205 /** 206 * Builds the table with actions which can be applied to the currently visible changesets 207 * in the changeset table. 208 * 209 * @return changset actions panel 210 */ 211 protected JPanel buildChangesetTableActionPanel() { 212 JPanel pnl = new JPanel(new BorderLayout()); 213 214 JToolBar tb = new JToolBar(JToolBar.VERTICAL); 215 tb.setFloatable(false); 216 217 // -- remove from cache action 218 model.getSelectionModel().addListSelectionListener(actRemoveFromCacheAction); 219 tb.add(actRemoveFromCacheAction); 220 221 // -- close selected changesets action 222 model.getSelectionModel().addListSelectionListener(actCloseSelectedChangesetsAction); 223 tb.add(actCloseSelectedChangesetsAction); 224 225 // -- download selected changesets 226 model.getSelectionModel().addListSelectionListener(actDownloadSelectedChangesets); 227 tb.add(actDownloadSelectedChangesets); 228 229 // -- download the content of the selected changesets 230 model.getSelectionModel().addListSelectionListener(actDownloadSelectedContent); 231 tb.add(actDownloadSelectedContent); 232 233 pnl.add(tb, BorderLayout.CENTER); 234 return pnl; 235 } 236 237 /** 238 * Builds the panel with the table of changesets 239 * 240 * @return the panel with the table of changesets 241 */ 242 protected JPanel buildChangesetTablePanel() { 243 JPanel pnl = new JPanel(new BorderLayout()); 244 tblChangesets = new JTable( 245 model, 246 new ChangesetCacheTableColumnModel(), 247 model.getSelectionModel() 248 ); 249 tblChangesets.addMouseListener(new MouseEventHandler()); 250 tblChangesets.getInputMap(JComponent.WHEN_FOCUSED).put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER,0), "showDetails"); 251 tblChangesets.getActionMap().put("showDetails", new ShowDetailAction()); 252 model.getSelectionModel().addListSelectionListener(new ChangesetDetailViewSynchronizer()); 253 254 // activate DEL on the table 255 tblChangesets.getInputMap(JComponent.WHEN_FOCUSED).put(KeyStroke.getKeyStroke(KeyEvent.VK_DELETE,0), "removeFromCache"); 256 tblChangesets.getActionMap().put("removeFromCache", actRemoveFromCacheAction); 257 258 pnl.add(new JScrollPane(tblChangesets), BorderLayout.CENTER); 259 pnl.add(buildChangesetTableActionPanel(), BorderLayout.WEST); 260 return pnl; 261 } 262 263 protected void build() { 264 setTitle(tr("Changeset Management Dialog")); 265 setIconImage(ImageProvider.get("dialogs/changeset", "changesetmanager").getImage()); 266 Container cp = getContentPane(); 267 268 cp.setLayout(new BorderLayout()); 269 270 buildModel(); 271 cp.add(buildToolbarPanel(), BorderLayout.NORTH); 272 cp.add(buildContentPanel(), BorderLayout.CENTER); 273 cp.add(buildButtonPanel(), BorderLayout.SOUTH); 274 275 // the help context 276 HelpUtil.setHelpContext(getRootPane(), HelpUtil.ht("/Dialog/ChangesetCacheManager")); 277 278 // make the dialog respond to ESC 279 getRootPane().getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE,0), "cancelAndClose"); 280 getRootPane().getActionMap().put("cancelAndClose", new CancelAction()); 281 282 // install a window event handler 283 addWindowListener(new WindowEventHandler()); 284 } 285 286 public ChangesetCacheManager() { 287 build(); 288 } 289 290 @Override 291 public void setVisible(boolean visible) { 292 if (visible) { 293 new WindowGeometry( 294 getClass().getName() + ".geometry", 295 WindowGeometry.centerInWindow( 296 getParent(), 297 new Dimension(1000,600) 298 ) 299 ).applySafe(this); 300 needsSplitPaneAdjustment = true; 301 model.init(); 302 303 } else if (isShowing()) { // Avoid IllegalComponentStateException like in #8775 304 model.tearDown(); 305 new WindowGeometry(this).remember(getClass().getName() + ".geometry"); 306 } 307 super.setVisible(visible); 308 } 309 310 /** 311 * Handler for window events 312 * 313 */ 314 class WindowEventHandler extends WindowAdapter { 315 @Override 316 public void windowClosing(WindowEvent e) { 317 new CancelAction().cancelAndClose(); 318 } 319 320 @Override 321 public void windowActivated(WindowEvent arg0) { 322 if (needsSplitPaneAdjustment) { 323 spContent.setDividerLocation(0.5); 324 needsSplitPaneAdjustment = false; 325 } 326 } 327 } 328 329 /** 330 * the cancel / close action 331 */ 332 static class CancelAction extends AbstractAction { 333 public CancelAction() { 334 putValue(NAME, tr("Close")); 335 putValue(SMALL_ICON, ImageProvider.get("cancel")); 336 putValue(SHORT_DESCRIPTION, tr("Close the dialog")); 337 } 338 339 public void cancelAndClose() { 340 destroyInstance(); 341 } 342 343 @Override 344 public void actionPerformed(ActionEvent arg0) { 345 cancelAndClose(); 346 } 347 } 348 349 /** 350 * The action to query and download changesets 351 */ 352 class QueryAction extends AbstractAction { 353 public QueryAction() { 354 putValue(NAME, tr("Query")); 355 putValue(SMALL_ICON, ImageProvider.get("dialogs","search")); 356 putValue(SHORT_DESCRIPTION, tr("Launch the dialog for querying changesets")); 357 } 358 359 @Override 360 public void actionPerformed(ActionEvent evt) { 361 ChangesetQueryDialog dialog = new ChangesetQueryDialog(ChangesetCacheManager.this); 362 dialog.initForUserInput(); 363 dialog.setVisible(true); 364 if (dialog.isCanceled()) 365 return; 366 367 try { 368 ChangesetQuery query = dialog.getChangesetQuery(); 369 if (query == null) return; 370 ChangesetQueryTask task = new ChangesetQueryTask(ChangesetCacheManager.this, query); 371 ChangesetCacheManager.getInstance().runDownloadTask(task); 372 } catch (IllegalStateException e) { 373 JOptionPane.showMessageDialog(ChangesetCacheManager.this, e.getMessage(), tr("Error"), JOptionPane.ERROR_MESSAGE); 374 } 375 } 376 } 377 378 /** 379 * Removes the selected changesets from the local changeset cache 380 * 381 */ 382 class RemoveFromCacheAction extends AbstractAction implements ListSelectionListener{ 383 public RemoveFromCacheAction() { 384 putValue(NAME, tr("Remove from cache")); 385 putValue(SMALL_ICON, ImageProvider.get("dialogs", "delete")); 386 putValue(SHORT_DESCRIPTION, tr("Remove the selected changesets from the local cache")); 387 updateEnabledState(); 388 } 389 390 @Override 391 public void actionPerformed(ActionEvent arg0) { 392 List<Changeset> selected = model.getSelectedChangesets(); 393 ChangesetCache.getInstance().remove(selected); 394 } 395 396 protected void updateEnabledState() { 397 setEnabled(model.hasSelectedChangesets()); 398 } 399 400 @Override 401 public void valueChanged(ListSelectionEvent e) { 402 updateEnabledState(); 403 404 } 405 } 406 407 /** 408 * Closes the selected changesets 409 * 410 */ 411 class CloseSelectedChangesetsAction extends AbstractAction implements ListSelectionListener{ 412 public CloseSelectedChangesetsAction() { 413 putValue(NAME, tr("Close")); 414 putValue(SMALL_ICON, ImageProvider.get("closechangeset")); 415 putValue(SHORT_DESCRIPTION, tr("Close the selected changesets")); 416 updateEnabledState(); 417 } 418 419 @Override 420 public void actionPerformed(ActionEvent arg0) { 421 List<Changeset> selected = model.getSelectedChangesets(); 422 Main.worker.submit(new CloseChangesetTask(selected)); 423 } 424 425 protected void updateEnabledState() { 426 List<Changeset> selected = model.getSelectedChangesets(); 427 JosmUserIdentityManager im = JosmUserIdentityManager.getInstance(); 428 for (Changeset cs: selected) { 429 if (cs.isOpen()) { 430 if (im.isPartiallyIdentified() && cs.getUser() != null && cs.getUser().getName().equals(im.getUserName())) { 431 setEnabled(true); 432 return; 433 } 434 if (im.isFullyIdentified() && cs.getUser() != null && cs.getUser().getId() == im.getUserId()) { 435 setEnabled(true); 436 return; 437 } 438 } 439 } 440 setEnabled(false); 441 } 442 443 @Override 444 public void valueChanged(ListSelectionEvent e) { 445 updateEnabledState(); 446 } 447 } 448 449 /** 450 * Downloads the selected changesets 451 * 452 */ 453 class DownloadSelectedChangesetsAction extends AbstractAction implements ListSelectionListener{ 454 public DownloadSelectedChangesetsAction() { 455 putValue(NAME, tr("Update changeset")); 456 putValue(SMALL_ICON, ImageProvider.get("dialogs/changeset", "updatechangeset")); 457 putValue(SHORT_DESCRIPTION, tr("Updates the selected changesets with current data from the OSM server")); 458 updateEnabledState(); 459 } 460 461 @Override 462 public void actionPerformed(ActionEvent arg0) { 463 List<Changeset> selected = model.getSelectedChangesets(); 464 ChangesetHeaderDownloadTask task =ChangesetHeaderDownloadTask.buildTaskForChangesets(ChangesetCacheManager.this,selected); 465 ChangesetCacheManager.getInstance().runDownloadTask(task); 466 } 467 468 protected void updateEnabledState() { 469 setEnabled(model.hasSelectedChangesets()); 470 } 471 472 @Override 473 public void valueChanged(ListSelectionEvent e) { 474 updateEnabledState(); 475 } 476 } 477 478 /** 479 * Downloads the content of selected changesets from the OSM server 480 * 481 */ 482 class DownloadSelectedChangesetContentAction extends AbstractAction implements ListSelectionListener{ 483 public DownloadSelectedChangesetContentAction() { 484 putValue(NAME, tr("Download changeset content")); 485 putValue(SMALL_ICON, ImageProvider.get("dialogs/changeset", "downloadchangesetcontent")); 486 putValue(SHORT_DESCRIPTION, tr("Download the content of the selected changesets from the server")); 487 updateEnabledState(); 488 } 489 490 @Override 491 public void actionPerformed(ActionEvent arg0) { 492 ChangesetContentDownloadTask task = new ChangesetContentDownloadTask(ChangesetCacheManager.this,model.getSelectedChangesetIds()); 493 ChangesetCacheManager.getInstance().runDownloadTask(task); 494 } 495 496 protected void updateEnabledState() { 497 setEnabled(model.hasSelectedChangesets()); 498 } 499 500 @Override 501 public void valueChanged(ListSelectionEvent e) { 502 updateEnabledState(); 503 } 504 } 505 506 class ShowDetailAction extends AbstractAction { 507 508 public void showDetails() { 509 List<Changeset> selected = model.getSelectedChangesets(); 510 if (selected.size() != 1) return; 511 model.setChangesetInDetailView(selected.get(0)); 512 } 513 514 @Override 515 public void actionPerformed(ActionEvent arg0) { 516 showDetails(); 517 } 518 } 519 520 class DownloadMyChangesets extends AbstractAction { 521 public DownloadMyChangesets() { 522 putValue(NAME, tr("My changesets")); 523 putValue(SMALL_ICON, ImageProvider.get("dialogs/changeset", "downloadchangeset")); 524 putValue(SHORT_DESCRIPTION, tr("Download my changesets from the OSM server (max. 100 changesets)")); 525 } 526 527 protected void alertAnonymousUser() { 528 HelpAwareOptionPane.showOptionDialog( 529 ChangesetCacheManager.this, 530 tr("<html>JOSM is currently running with an anonymous user. It cannot download<br>" 531 + "your changesets from the OSM server unless you enter your OSM user name<br>" 532 + "in the JOSM preferences.</html>" 533 ), 534 tr("Warning"), 535 JOptionPane.WARNING_MESSAGE, 536 HelpUtil.ht("/Dialog/ChangesetCacheManager#CanDownloadMyChangesets") 537 ); 538 } 539 540 @Override 541 public void actionPerformed(ActionEvent arg0) { 542 JosmUserIdentityManager im = JosmUserIdentityManager.getInstance(); 543 if (im.isAnonymous()) { 544 alertAnonymousUser(); 545 return; 546 } 547 ChangesetQuery query = new ChangesetQuery(); 548 if (im.isFullyIdentified()) { 549 query = query.forUser(im.getUserId()); 550 } else { 551 query = query.forUser(im.getUserName()); 552 } 553 ChangesetQueryTask task = new ChangesetQueryTask(ChangesetCacheManager.this, query); 554 ChangesetCacheManager.getInstance().runDownloadTask(task); 555 } 556 } 557 558 class MouseEventHandler extends PopupMenuLauncher { 559 560 public MouseEventHandler() { 561 super(new ChangesetTablePopupMenu()); 562 } 563 564 @Override 565 public void mouseClicked(MouseEvent evt) { 566 if (isDoubleClick(evt)) { 567 new ShowDetailAction().showDetails(); 568 } 569 } 570 } 571 572 class ChangesetTablePopupMenu extends JPopupMenu { 573 public ChangesetTablePopupMenu() { 574 add(actRemoveFromCacheAction); 575 add(actCloseSelectedChangesetsAction); 576 add(actDownloadSelectedChangesets); 577 add(actDownloadSelectedContent); 578 } 579 } 580 581 class ChangesetDetailViewSynchronizer implements ListSelectionListener { 582 @Override 583 public void valueChanged(ListSelectionEvent e) { 584 List<Changeset> selected = model.getSelectedChangesets(); 585 if (selected.size() == 1) { 586 model.setChangesetInDetailView(selected.get(0)); 587 } else { 588 model.setChangesetInDetailView(null); 589 } 590 } 591 } 592 593 /** 594 * Selects the changesets in <code>changests</code>, provided the 595 * respective changesets are already present in the local changeset cache. 596 * 597 * @param changesets the collection of changesets. If {@code null}, the 598 * selection is cleared. 599 */ 600 public void setSelectedChangesets(Collection<Changeset> changesets) { 601 model.setSelectedChangesets(changesets); 602 int idx = model.getSelectionModel().getMinSelectionIndex(); 603 if (idx < 0) return; 604 tblChangesets.scrollRectToVisible(tblChangesets.getCellRect(idx, 0, true)); 605 repaint(); 606 } 607 608 /** 609 * Selects the changesets with the ids in <code>ids</code>, provided the 610 * respective changesets are already present in the local changeset cache. 611 * 612 * @param ids the collection of ids. If null, the selection is cleared. 613 */ 614 public void setSelectedChangesetsById(Collection<Integer> ids) { 615 if (ids == null) { 616 setSelectedChangesets(null); 617 return; 618 } 619 Set<Changeset> toSelect = new HashSet<Changeset>(); 620 ChangesetCache cc = ChangesetCache.getInstance(); 621 for (int id: ids) { 622 if (cc.contains(id)) { 623 toSelect.add(cc.get(id)); 624 } 625 } 626 setSelectedChangesets(toSelect); 627 } 628 629 public void runDownloadTask(final ChangesetDownloadTask task) { 630 Main.worker.submit(task); 631 Runnable r = new Runnable() { 632 @Override public void run() { 633 if (task.isCanceled() || task.isFailed()) return; 634 setSelectedChangesets(task.getDownloadedChangesets()); 635 } 636 }; 637 Main.worker.submit(r); 638 } 639}