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