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