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