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